Compare commits

...
Author SHA1 Message Date
Brandon Thomas 957d510a04 wip 2026-01-29 19:44:07 -05:00
Brandon Thomas 20bf5608eb create session 2026-01-29 10:28:39 -05:00
Brandon Thomas 03e5d735d8 pass payload back 2026-01-29 10:15:30 -05:00
Brandon Thomas 114171f985 wip 2026-01-29 10:05:30 -05:00
Brandon Thomas 17fbc3f841 plumbing database fields 2026-01-29 09:58:06 -05:00
Brandon Thomas 972420b5d5 wip 2026-01-29 09:26:04 -05:00
Brandon Thomas 6face4ab4f undo change 2026-01-29 02:39:54 -05:00
Brandon Thomas dffc831ce1 wip 2026-01-29 02:24:34 -05:00
Brandon Thomas 671ac9310f endpoints wip 2026-01-28 21:25:59 -05:00
Brandon Thomas a752c64a99 new users table fields 2026-01-28 20:01:31 -05:00
54 changed files with 1184 additions and 142 deletions
Generated
+35
View File
@@ -330,6 +330,24 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "actix_artcraft"
version = "0.0.1"
dependencies = [
"actix-http",
"actix-service",
"actix-web",
"actix_helpers",
"anyhow",
"cookies",
"enums",
"errors",
"http_headers",
"log",
"tokens",
"workspace-build-acceleration",
]
[[package]]
name = "actix_cors_configs"
version = "0.0.1"
@@ -1351,6 +1369,7 @@ version = "0.0.1"
dependencies = [
"actix-http",
"actix-web",
"actix_artcraft",
"anyhow",
"artcraft_api_defs",
"async-stripe 1.0.0-alpha.2",
@@ -1364,6 +1383,7 @@ dependencies = [
"component_traits",
"enums",
"errors",
"http_headers",
"http_server_common",
"log",
"mockall",
@@ -1377,6 +1397,7 @@ dependencies = [
"tokens",
"tokio",
"user_traits_component",
"users",
"workspace-build-acceleration",
]
@@ -10718,6 +10739,7 @@ dependencies = [
"actix-web",
"actix-web-actors",
"actix-web-lab",
"actix_artcraft",
"actix_cors_configs",
"actix_helpers",
"anyhow",
@@ -10807,6 +10829,7 @@ dependencies = [
"url_config",
"user_input_common",
"user_traits_component",
"users",
"utoipa",
"utoipa-swagger-ui",
"videos",
@@ -12673,6 +12696,18 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "users"
version = "0.0.1"
dependencies = [
"collections",
"md-5 0.10.6",
"once_cell",
"primitives",
"rand 0.9.1",
"workspace-build-acceleration",
]
[[package]]
name = "utf-8"
version = "0.7.6"
+8
View File
@@ -36,6 +36,7 @@ members = [
"crates/desktop/artcraft",
# Shared libraries
"crates/lib/actix_artcraft",
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/bootstrap",
@@ -89,6 +90,7 @@ members = [
"crates/lib/url_config",
"crates/lib/url_utils",
"crates/lib/user_input_common",
"crates/lib/users",
"crates/lib/web_scrapers",
#"crates/lib/api_clients/fakeyou_client",
#"crates/lib/machine_learning/ml_models",
@@ -185,13 +187,17 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# Internal libs
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
# It makes refactoring much easier.
actix_artcraft = { path = "crates/lib/actix_artcraft" }
actix_helpers = { path = "crates/lib/actix_helpers" }
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
collections = { path = "crates/lib/data/collections" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
cookies = { path = "crates/lib/cookies" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
@@ -218,6 +224,7 @@ test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
web_base64 = { path = "crates/lib/data/web_base64" }
world_labs_client = { path = "crates/api_clients/world_labs_client" }
@@ -246,6 +253,7 @@ futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
md-5 = "0.10.6" # insecure but used for gravatar
once_cell = "1.21.3"
r2d2 = "0.8.10"
rand = "0.9.1"
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
was_eagerly_provisioned;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
is_temporary;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
is_temporary BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
email_is_synthetic;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
email_is_synthetic BOOLEAN NOT NULL DEFAULT false;
@@ -12,6 +12,14 @@ CREATE TABLE users (
-- Visible "primary key"
token VARCHAR(32) NOT NULL,
-- This is TRUE for accounts that were provisioned automatically (eg. eager stripe
-- checkout flow), and who haven't completed onboarding (eg. finishing with billing).
-- Once the user finishes setup, this is set to FALSE.
-- Existing users and normal signup flow always sets this to FALSE.
-- If enough time has elapsed for an incomplete `is_temporary` user, such user records
-- might be possible to delete as we assume the user abandoned sign up long ago.
is_temporary BOOLEAN NOT NULL DEFAULT false,
-- Username is a lookup key; display_name allows the user to add custom case.
username VARCHAR(20) NOT NULL,
display_name VARCHAR(20) NOT NULL,
@@ -30,6 +38,8 @@ CREATE TABLE users (
-- Set by the user at sign up:
-- - When email + password signup, this is the email provided.
-- - When Google SSO, this is the email provided by Google.
-- - When "eager" checkout flow, this is synthetically generated and
-- updated by the user later.
email_address VARCHAR(255) NOT NULL,
-- Email address is confirmed by us.
@@ -39,6 +49,11 @@ CREATE TABLE users (
-- Email address is confirmed by Google.
email_confirmed_by_google BOOLEAN NOT NULL DEFAULT false,
-- We created a user account provisionally, and the email address is not yet real.
-- This happens during "eager" checkout flow before users create accounts - we create
-- a user account for them with a fake email address (since this is a unique key).
email_is_synthetic BOOLEAN NOT NULL DEFAULT false,
-- The role assigned to the user confers permissions.
user_role_slug VARCHAR(16) NOT NULL,
@@ -86,6 +101,7 @@ CREATE TABLE users (
password_version INT UNSIGNED NOT NULL DEFAULT 0,
-- Users signing up from SSO do not have a password at time of account creation.
-- Users signing up with eager stripe checkout flow do not have a password at time of creation
is_without_password BOOLEAN NOT NULL DEFAULT false,
-- ========== ABUSE TRACKING ==========
@@ -191,10 +207,19 @@ CREATE TABLE users (
maybe_source VARCHAR(255) DEFAULT NULL,
-- How users created their account
-- Initially this will be "email_password" or "google_sign_in"
-- Older users do not have a value and are assumed to be "email_sign_in"
-- Older users do not have a value and are assumed to be "email_password"
-- Values:
-- - "email_password"
-- - "google_sign_in"
-- - "stripe_checkout" (eager stripe checkout account provisioning)
maybe_signup_method VARCHAR(16) DEFAULT NULL,
-- Account was provisioned automatically (eg. stripe eager checkout flow)
-- This will remain true even if the user finishes account setup, email change,
-- password setup, etc. (It's a permanent label on the account for later
-- statistical evaluation.)
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false,
-- ========== MODERATION DETAILS ==========
-- Different than deleted.
@@ -0,0 +1,54 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_NEW_USER_ACCOUNT_AND_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/user_signup/subscription_checkout";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
/// The (non-Stripe) internal identifier for the product or subscription.
/// This will be translated into a Stripe identifier.
pub plan: Option<ArtcraftSubscriptionSlug>,
pub cadence: Option<PlanBillingCadence>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadence {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse {
pub success: bool,
/// The checkout session URL.
pub stripe_checkout_redirect_url: String,
/// If a user account was created, these are the details.
pub generated_user: Option<UserDetails>,
/// If a session was created, these are the details.
pub session: Option<SessionDetails>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UserDetails {
/// The generated username.
pub username: String,
/// The generated display name.
pub display_name: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SessionDetails {
/// A signed session that can be sent as a header, bypassing cookies.
/// This is useful for API clients that don't support cookies or Google
/// browsers killing cross-domain cookies.
pub signed_session: String,
}
@@ -1,3 +1,4 @@
pub mod create_create_new_user_account_and_subscription_checkout;
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
pub mod customer_portal_cancel_plan;
+40
View File
@@ -0,0 +1,40 @@
# For things that are too application-logic specific to live in generic actix helpers.
[package]
name = "actix_artcraft"
edition = "2024"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "actix_artcraft"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
actix_helpers.workspace = true
cookies.workspace = true
enums.workspace = true
errors.workspace = true
http_headers.workspace = true
tokens.workspace = true
# External
actix-http.workspace = true
actix-service.workspace = true
actix-web.workspace = true
log.workspace = true
anyhow.workspace = true
[dev-dependencies]
# (None yet)
+21
View File
@@ -0,0 +1,21 @@
//! actix_artcraft - for actix helpers that need to be shared between crates,
//! but are too application-specific to be in general actix helper crates
//!
// Never allow these
#![forbid(private_bounds)]
#![forbid(private_interfaces)]
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
// Okay to toggle
#![forbid(unreachable_patterns)]
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
// Always allow
#![allow(dead_code)]
#![allow(non_snake_case)]
pub mod requests;
pub mod sessions;
@@ -1,6 +1,5 @@
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
use actix_web::HttpRequest;
use enums::by_table::users::user_signup_source::UserSignupSource;
use log::warn;
const ARTCRAFT : &str = "artcraft";
@@ -50,10 +49,6 @@ pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static
None
}
pub fn get_request_signup_source_enum(http_request: &HttpRequest) -> Option<UserSignupSource> {
get_request_signup_source(http_request)
.and_then(|source_str| UserSignupSource::from_str(source_str).ok())
}
#[cfg(test)]
mod tests {
@@ -61,7 +56,7 @@ mod tests {
mod origin_header_str {
use super::*;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
use crate::requests::get_request_signup_source::get_request_signup_source;
#[test]
fn artcraft_dot_ai() {
@@ -119,66 +114,4 @@ mod tests {
assert_eq!(get_request_signup_source(&request), Some("storyteller"));
}
}
mod origin_header_enum {
use super::*;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source_enum;
use enums::by_table::users::user_signup_source::UserSignupSource;
#[test]
fn artcraft_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://artcraft.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn api_dot_get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn api_dot_fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
#[test]
fn api_dot_storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
}
}
@@ -0,0 +1,75 @@
use crate::requests::get_request_signup_source::get_request_signup_source;
use actix_web::HttpRequest;
use enums::by_table::users::user_signup_source::UserSignupSource;
pub fn get_request_signup_source_enum(http_request: &HttpRequest) -> Option<UserSignupSource> {
get_request_signup_source(http_request)
.and_then(|source_str| UserSignupSource::from_str(source_str).ok())
}
#[cfg(test)]
mod tests {
use actix_web::test::TestRequest;
mod origin_header_enum {
use super::*;
use crate::requests::get_request_signup_source_enum::get_request_signup_source_enum;
use enums::by_table::users::user_signup_source::UserSignupSource;
#[test]
fn artcraft_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://artcraft.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn api_dot_get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn api_dot_fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
#[test]
fn api_dot_storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
}
}
@@ -0,0 +1,2 @@
pub mod get_request_signup_source;
pub mod get_request_signup_source_enum;
@@ -9,13 +9,12 @@ use actix_web::HttpRequest;
use anyhow::anyhow;
use log::warn;
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
use errors::AnyhowResult;
use tokens::tokens::user_sessions::UserSessionToken;
use tokens::tokens::users::UserToken;
use crate::http_server::session::http::http_user_session_payload::HttpUserSessionPayload;
use crate::http_server::session::http::payload_signer::HttpUserSessionPayloadSigner;
/// Name of the HTTP cookie that carries the session payload
const SESSION_COOKIE_NAME : &str = "session";
@@ -139,7 +138,7 @@ mod tests {
use tokens::tokens::user_sessions::UserSessionToken;
use tokens::tokens::users::UserToken;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::sessions::http_user_session_manager::HttpUserSessionManager;
#[test]
fn test_create_cookie_payload() {
@@ -1,3 +1,3 @@
pub mod http_user_session_manager;
pub mod http_user_session_payload;
pub mod payload_signer;
pub mod payload_signer;
@@ -1,12 +1,11 @@
use std::collections::BTreeMap;
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
use cookies::jwt_signer::JwtSigner;
use errors::AnyhowResult;
use tokens::tokens::user_sessions::UserSessionToken;
use tokens::tokens::users::UserToken;
use crate::http_server::session::http::http_user_session_payload::HttpUserSessionPayload;
/**
* Session payload version history
*
@@ -57,11 +56,10 @@ impl HttpUserSessionPayloadSigner {
#[cfg(test)]
mod tests {
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
use tokens::tokens::user_sessions::UserSessionToken;
use tokens::tokens::users::UserToken;
use crate::http_server::session::http::payload_signer::HttpUserSessionPayloadSigner;
#[test]
fn test_encode() {
let signer = HttpUserSessionPayloadSigner::new("fake_secret").unwrap();
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "users"
edition = "2024"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "users"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
collections.workspace = true
primitives.workspace = true
# External
md-5.workspace = true
once_cell.workspace = true
rand.workspace = true
[dev-dependencies]
# (None)
@@ -13,7 +13,7 @@ pub fn email_to_gravatar(email_address: &str) -> String {
#[cfg(test)]
pub mod tests {
use crate::util::email_to_gravatar::email_to_gravatar;
use crate::email::email_to_gravatar::email_to_gravatar;
#[test]
fn test_gravatar() {
@@ -0,0 +1,29 @@
use rand::Rng;
const EMAIL_PREFIX : &str = "synthetic.email";
const EMAIL_DOMAIN : &str = "getartcraft.com";
pub fn generate_random_synthetic_email() -> String {
let random_digit = rand::thread_rng().gen_range(u32::MIN..u32::MAX);
format!("{}.{}@{}", EMAIL_PREFIX, random_digit, EMAIL_DOMAIN)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::email::generate_random_synthetic_email::generate_random_synthetic_email;
#[test]
fn test_base_case_success() {
assert!(generate_random_synthetic_email().len() > 0);
}
#[test]
fn generate_lots() {
let mut collection = HashSet::new();
for _ in 0..100 {
collection.insert(generate_random_synthetic_email());
}
assert!(collection.len() > 50); // NB: Should be an easy bar to hit
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod email_to_gravatar;
pub mod generate_random_synthetic_email;
+20
View File
@@ -0,0 +1,20 @@
//! users
//!
// Never allow these
#![forbid(private_bounds)]
#![forbid(private_interfaces)]
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
// Okay to toggle
#![forbid(unreachable_patterns)]
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
// Always allow
#![allow(dead_code)]
#![allow(non_snake_case)]
pub mod email;
pub mod username;
@@ -0,0 +1,3 @@
/// Username may be up to this many characters, but not more.
pub const USERNAME_MAX_LENGTH: usize = 16;
@@ -1,15 +1,15 @@
use crate::http_server::validations::validate_username::USERNAME_MAX_LENGTH;
use crate::username::constants::USERNAME_MAX_LENGTH;
use collections::random_from_array::random_from_array;
use once_cell::sync::Lazy;
use primitives::iterators::iterate_trimmed_lines_without_comments::iterate_trimmed_lines_without_comments;
use primitives::str::first_letter_uppercase::first_letter_uppercase;
use rand::distr::{Distribution, StandardUniform};
use rand::Rng;
use std::collections::HashSet;
use rand::distr::{Distribution, StandardUniform};
pub const ADJECTIVES : &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_adjectives.txt");
pub const NOUNS : &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_nouns.txt");
pub const NOUNS_ANIMALS: &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_nouns_animals.txt");
pub const ADJECTIVES : &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_adjectives.txt");
pub const NOUNS : &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_nouns.txt");
pub const NOUNS_ANIMALS: &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_nouns_animals.txt");
static ALL_NOUNS : Lazy<Vec<&'static str>> = Lazy::new(|| {
iterate_trimmed_lines_without_comments(NOUNS.lines())
@@ -120,7 +120,7 @@ fn maybe_random_safe_digit() -> Option<u32> {
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::util::generate_random_username::generate_random_username;
use crate::username::generate_random_username::generate_random_username;
#[test]
fn test_base_case_success() {
+2
View File
@@ -0,0 +1,2 @@
pub mod generate_random_username;
pub mod constants;
@@ -64,6 +64,11 @@ pub async fn create_account_from_email_and_password(
// NB: This is just for testing.
maybe_user_token: args.maybe_user_token,
// These flags represent eagerly created users and are false for this flow
is_temporary: false,
email_is_synthetic: false,
was_eagerly_provisioned: false,
},
Transactor::for_pool(mysql_pool),
).await?;
@@ -59,6 +59,11 @@ pub async fn create_account_from_google_sso(
// NB: This is just for testing.
maybe_user_token: None,
// These flags represent eagerly created users and are false for this flow
is_temporary: false,
email_is_synthetic: false,
was_eagerly_provisioned: false,
},
transactor,
).await?;
@@ -0,0 +1,78 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use crate::queries::users::user::create::create_account_error::CreateAccountError;
use crate::queries::users::user::create::create_account_generic::{create_account_generic, GenericCreateAccountArgs};
use crate::utils::transactor::Transactor;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use enums::by_table::users::user_signup_source::UserSignupSource;
use tokens::tokens::users::UserToken;
/// Stripe checkout sessions do not have a real email address or password.
/// The user will create these once the checkout session is completed.
/// The password hash field is nullable, so we can't leave it null/empty.
const SSO_PASSWORD : &str = "*";
pub struct CreateAccountFromStripeCheckoutArgs<'a> {
// NB: Username will likely be generated by our code, not the user.
pub username: &'a str,
pub display_name: &'a str,
// NB: Email will likely be generated by our code, not the user.
pub email_address: &'a str,
pub email_gravatar_hash: &'a str,
pub ip_address: &'a str,
pub maybe_source: Option<UserSignupSource>,
/// Comma separated string of feature flags.
pub maybe_feature_flags: Option<&'a str>,
}
pub async fn create_account_from_stripe_checkout(
args: CreateAccountFromStripeCheckoutArgs<'_>,
transactor: Transactor<'_, '_>,
) -> Result<UserToken, CreateAccountError>
{
let result= create_account_generic(
GenericCreateAccountArgs {
maybe_signup_method: Some(UserSignupMethod::StripeCheckout),
ip_address: args.ip_address,
maybe_feature_flags: args.maybe_feature_flags,
maybe_source: args.maybe_source,
// These are synthetic user accounts that are being eagerly created
// If the user abandons the onboarding flow, they can be deleted later.
is_temporary: true,
was_eagerly_provisioned: true,
// Stripe checkout users have randomly generated usernames
username: args.username,
display_name: args.display_name,
username_is_generated: true,
username_is_not_customized: true,
// Stripe checkout users have NO REAL EMAIL ADDRESS yet.
// Stripe will tell us what their email address is once
// they complete the checkout (or the user can update it,
// and their write wins.)
email_address: args.email_address,
email_gravatar_hash: args.email_gravatar_hash,
email_confirmed_by_google: false,
email_is_synthetic: true,
// SSO accounts start without a password
password_hash: SSO_PASSWORD,
is_without_password: true,
// NB: This is just for testing.
maybe_user_token: None,
},
transactor,
).await?;
Ok(result.user_token)
}
@@ -23,10 +23,35 @@ pub struct GenericCreateAccountArgs<'a> {
pub email_address: &'a str,
pub email_gravatar_hash: &'a str,
/// Google did email IDV and told us it's legitimate.
pub email_confirmed_by_google: bool,
/// The email address is not real yet. We generated a fake/synthetic value
/// that will hopefully be replaced later. This is for automated user creation
/// flows, like Stripe Checkout onboarding flow.
pub email_is_synthetic: bool,
pub password_hash: &'a str,
pub is_without_password: bool,
/// This is initially TRUE for accounts that were provisioned automatically,
/// without user intervention, e.g. in the eager Stripe Checkout onboarding
/// flow. Accounts in this state have not yet been customized by users and may
/// not in fact represent real users if the users abandon the flow. Once the
/// user finishes setup, this will be set to false.
///
/// Existing and normal setup flow sets this flag to false.
///
/// If enough time has passed without the user interacting with the account,
/// then it might be prudent to delete these records.
pub is_temporary: bool,
/// Whether the account was created eagerly (e.g. via stripe checkout flow)
/// without the user explicitly setting up an account, choosing an email or
/// password, etc. (It's a technical choice to do faster checkout flow.)
/// This is a permanent label on the account.
pub was_eagerly_provisioned: bool,
pub ip_address: &'a str,
pub maybe_source: Option<UserSignupSource>,
@@ -66,6 +91,9 @@ pub async fn create_account_generic(
INSERT INTO users
SET
token = ?,
is_temporary = ?,
username = ?,
display_name = ?,
@@ -77,6 +105,9 @@ SET
email_confirmed = FALSE,
email_confirmed_by_google = ?,
email_is_synthetic = ?,
was_eagerly_provisioned = ?,
profile_markdown = ?,
profile_rendered_html = ?,
@@ -96,6 +127,9 @@ SET
maybe_signup_method = ?
"#,
user_token.as_str(),
args.is_temporary,
args.username,
args.display_name,
@@ -106,6 +140,9 @@ SET
args.email_gravatar_hash,
args.email_confirmed_by_google,
args.email_is_synthetic,
args.was_eagerly_provisioned,
INITIAL_PROFILE_MARKDOWN,
INITIAL_PROFILE_RENDERED_HTML,
@@ -3,3 +3,4 @@ pub mod create_account_from_email_and_password;
pub mod create_account_from_google_sso;
pub (crate) mod create_account_generic;
pub mod create_account_from_stripe_checkout;
@@ -18,6 +18,11 @@ pub enum UserSignupMethod {
/// "Sign in With Google" SSO
GoogleSignIn,
/// Stripe Checkout flow, where we provision user accounts for users with a
/// synthetic/fake email address and no password. After checkout completes,
/// the user gets a real email and password - or the user can set them.
StripeCheckout,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -31,6 +36,7 @@ impl UserSignupMethod {
match self {
Self::EmailPassword => "email_password",
Self::GoogleSignIn=> "google_sign_in",
Self::StripeCheckout => "stripe_checkout",
}
}
@@ -38,6 +44,7 @@ impl UserSignupMethod {
match value {
"email_password" => Ok(Self::EmailPassword),
"google_sign_in" => Ok(Self::GoogleSignIn),
"stripe_checkout" => Ok(Self::StripeCheckout),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -48,6 +55,7 @@ impl UserSignupMethod {
BTreeSet::from([
Self::EmailPassword,
Self::GoogleSignIn,
Self::StripeCheckout,
])
}
}
@@ -64,6 +72,7 @@ mod tests {
fn test_serialization() {
assert_serialization(UserSignupMethod::EmailPassword, "email_password");
assert_serialization(UserSignupMethod::GoogleSignIn, "google_sign_in");
assert_serialization(UserSignupMethod::StripeCheckout, "stripe_checkout");
}
}
@@ -74,12 +83,14 @@ mod tests {
fn to_str() {
assert_eq!(UserSignupMethod::EmailPassword.to_str(), "email_password");
assert_eq!(UserSignupMethod::GoogleSignIn.to_str(), "google_sign_in");
assert_eq!(UserSignupMethod::StripeCheckout.to_str(), "stripe_checkout");
}
#[test]
fn from_str() {
assert_eq!(UserSignupMethod::from_str("email_password").unwrap(), UserSignupMethod::EmailPassword);
assert_eq!(UserSignupMethod::from_str("google_sign_in").unwrap(), UserSignupMethod::GoogleSignIn);
assert_eq!(UserSignupMethod::from_str("stripe_checkout").unwrap(), UserSignupMethod::StripeCheckout);
assert!(UserSignupMethod::from_str("foo").is_err());
}
}
@@ -90,9 +101,10 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = UserSignupMethod::all_variants();
assert_eq!(variants.len(), 2);
assert_eq!(variants.len(), 3);
assert_eq!(variants.pop_first(), Some(UserSignupMethod::EmailPassword));
assert_eq!(variants.pop_first(), Some(UserSignupMethod::GoogleSignIn));
assert_eq!(variants.pop_first(), Some(UserSignupMethod::StripeCheckout));
assert_eq!(variants.pop_first(), None);
}
}
@@ -21,10 +21,12 @@ normal = ["workspace-build-acceleration"]
workspace-build-acceleration.workspace = true
# Internal
actix_artcraft.workspace = true
artcraft_api_defs.workspace = true
component_traits.workspace = true
enums.workspace = true
errors.workspace = true
http_headers.workspace = true
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
reusable_types = { path = "../../../schema/public/reusable_types" }
@@ -42,6 +44,7 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] }
users.workspace = true
# Money
async-stripe = { version = "1.0.0-alpha.2", default-features = false, features = ["rustls-tls-native"] }
@@ -0,0 +1,16 @@
use stripe_shared::CheckoutSession;
use tokens::tokens::user_sessions::UserSessionToken;
use tokens::tokens::users::UserToken;
pub (super) struct CreationPayload {
pub checkout_session: CheckoutSession,
pub maybe_user_metadata: Option<UserMetadata>
}
pub (super) struct UserMetadata {
pub user_token: UserToken,
pub session_token: UserSessionToken,
pub username: String,
pub display_name: String,
pub email_address: String,
}
@@ -0,0 +1,4 @@
pub mod stripe_artcraft_create_subscription_checkout_with_user_signup_handler;
pub (super) mod user_exists_case;
pub (super) mod user_creation_case;
pub (super) mod creation_payload;
@@ -0,0 +1,183 @@
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
use crate::configs::subscriptions::get_artcraft_subscription_by_slug_and_env::get_artcraft_subscription_by_slug_and_env;
use crate::endpoints::checkout_with_user_signup::user_creation_case::user_creation_case;
use crate::endpoints::checkout_with_user_signup::user_exists_case::user_exists_case;
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use crate::utils::common_web_error::CommonWebError;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::web::{Data, Json};
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::stripe_artcraft::create_create_new_user_account_and_subscription_checkout::{PlanBillingCadence, SessionDetails, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse, UserDetails};
use http_headers::
use component_traits::traits::internal_user_lookup::InternalUserLookup;
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use enums::common::payments_namespace::PaymentsNamespace;
use log::{error, info, warn};
use mysql_queries::queries::users::user_stripe_customer_links::find_user_stripe_customer_link::find_user_stripe_customer_link_using_connection;
use mysql_queries::queries::users::user_subscriptions::find_possibly_inactive_first_subscription_for_owner_user::find_possibly_inactive_first_subscription_for_owner_user_using_connection;
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use reusable_types::server_environment::ServerEnvironment;
use sqlx::MySqlPool;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData};
use stripe_checkout::CheckoutSessionMode;
use stripe_core::CustomerId;
use stripe_shared::CheckoutSession;
use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON;
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
use crate::endpoints::checkout_with_user_signup::creation_payload::{CreationPayload, UserMetadata};
// /// Create a new user account and Stripe Checkout session and return the redirect URL in Json.
// #[utoipa::path(
// get,
// tag = "Stripe (Artcraft)",
// path = "/v1/stripe_artcraft/user_signup/subscription_checkout",
// params(
// ("request" = CreateCheckoutSessionRequest, description = "Payload for Request"),
// ),
// responses(
// (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse),
// ),
// )]
/// Create a Stripe Checkout session *with* user signup and return the redirect URL in Json.
/// If the user is already logged in, we just attach it to that record instead.
pub async fn stripe_artcraft_create_checkout_with_user_signup_handler(
http_request: HttpRequest,
request: Json<StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest>,
stripe_config: Data<ArtcraftStripeConfigWithClient>,
server_environment: Data<ServerEnvironment>,
session_cookie_manager: web::Data<HttpUserSessionManager>,
internal_user_lookup: Data<dyn InternalUserLookup>,
internal_session_cache_purge: Data<dyn InternalSessionCachePurge>,
mysql_pool: Data<MySqlPool>,
) -> Result<HttpResponse, CommonWebError>
{
let slug = match request.plan {
None => return Err(CommonWebError::BadInputWithSimpleMessage("no plan supplied".to_string())),
Some(slug) => slug,
};
let cadence = match request.cadence {
None => return Err(CommonWebError::BadInputWithSimpleMessage("no cadence supplied".to_string())),
Some(cadence) => cadence,
};
let plan = get_artcraft_subscription_by_slug_and_env(slug, **server_environment);
let mut mysql_connection = mysql_pool
.acquire()
.await
.map_err(|err| {
error!("Could not acquire mysql connection: {:?}", err);
CommonWebError::ServerError
})?;
let price_id = match cadence {
PlanBillingCadence::Monthly => plan.monthly_price_id.clone(),
PlanBillingCadence::Yearly => plan.yearly_price_id.clone(),
};
let maybe_user_metadata = internal_user_lookup
.lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection)
.await
.map_err(|err| {
error!("Error looking up user: {:?}", err);
CommonWebError::ServerError // NB: This was probably *our* fault.
})?;
let creation_payload= match maybe_user_metadata {
None => {
info!("Creating new user, then creating checkout session...");
user_creation_case(
&http_request,
&price_id,
&mut mysql_connection,
&stripe_config,
).await?
},
Some(user_metadata) => {
info!("Creating checkout session for user: {:?}", user_metadata.user_token_typed);
user_exists_case(
&price_id,
&user_metadata,
&mut mysql_connection,
&stripe_config,
).await?
},
};
// Best effort to delete Redis session cache
internal_session_cache_purge.best_effort_purge_session_cache(&http_request);
match creation_payload.maybe_user_metadata {
None => create_http_response_existing_user(creation_payload.checkout_session)
Some(_) => {}
}
}
pub fn create_http_response_existing_user(
checkout_session: CheckoutSession,
) -> Result<HttpResponse, CommonWebError> {
let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
let response = StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse {
success: true,
stripe_checkout_redirect_url: url,
generated_user: None,
session: None,
};
let body = serde_json::to_string(&response)
.map_err(|_e| CommonWebError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type(CONTENT_TYPE_APPLICATION_JSON)
.body(body))
}
pub fn create_http_response_new_user(
session_cookie_manager: &HttpUserSessionManager,
checkout_session: CheckoutSession,
user_metadata: UserMetadata,
) -> Result<HttpResponse, CommonWebError> {
let session_cookie = match session_cookie_manager.create_cookie(&user_metadata.session_token, &user_metadata.user_token) {
Ok(cookie) => cookie,
Err(err) => {
error!("Error creating session cookie: {:?}", err);
return Err(CommonWebError::ServerError)
},
};
let signed_session = match session_cookie_manager.encode_session_payload(&user_metadata.session_token, &user_metadata.user_token) {
Ok(payload) => payload,
Err(err) => {
error!("Error encoding session payload: {:?}", err);
return Err(CommonWebError::ServerError)
},
};
let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
let response = StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse {
success: true,
stripe_checkout_redirect_url: url,
generated_user: Some(UserDetails {
username: user_metadata.username,
display_name: user_metadata.display_name,
}),
session: Some(SessionDetails {
signed_session,
}),
};
let body = serde_json::to_string(&response)
.map_err(|_e| CommonWebError::ServerError)?;
Ok(HttpResponse::Ok()
.cookie(session_cookie)
.content_type(CONTENT_TYPE_APPLICATION_JSON)
.body(body))
}
@@ -0,0 +1,222 @@
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
use crate::endpoints::checkout_with_user_signup::creation_payload::{CreationPayload, UserMetadata};
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use crate::utils::common_web_error::CommonWebError;
use actix_artcraft::requests::get_request_signup_source_enum::get_request_signup_source_enum;
use actix_web::HttpRequest;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::users::user::create::create_account_error::CreateAccountError;
use mysql_queries::queries::users::user::create::create_account_from_google_sso::{create_account_from_google_sso, CreateAccountFromGoogleSsoArgs};
use mysql_queries::queries::users::user::create::create_account_from_stripe_checkout::{create_account_from_stripe_checkout, CreateAccountFromStripeCheckoutArgs};
use mysql_queries::utils::transactor::Transactor;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use std::collections::HashMap;
use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData};
use stripe_shared::{CheckoutSession, CheckoutSessionMode};
use mysql_queries::queries::users::user_sessions::create_user_session_with_transactor::create_user_session_with_transactor;
use users::email::email_to_gravatar::email_to_gravatar;
use users::email::generate_random_synthetic_email::generate_random_synthetic_email;
use users::username::generate_random_username::generate_random_username;
pub (super) async fn user_creation_case(
http_request: &HttpRequest,
price_id: &str,
mysql_connection: &mut PoolConnection<MySql>,
stripe_config: &ArtcraftStripeConfigWithClient,
) -> Result<CreationPayload, CommonWebError> {
let mut transaction = mysql_connection.begin()
.await
.map_err(|e| {
warn!("Could not begin transaction: {:?}", e);
CommonWebError::ServerError
})?;
let ip_address = get_request_ip(&http_request);
let maybe_source = get_request_signup_source_enum(&http_request);
// NB: Not sure if these lock important functionality, so we're keeping this
let user_feature_flags = vec![
UserFeatureFlag::Studio,
UserFeatureFlag::VideoStyleTransfer,
].into_iter()
.map(|flag| flag.to_string())
.collect::<Vec<String>>()
.join(",");
let mut final_user_token = None;
let mut final_username = None;
let mut final_display_name = None;
let mut final_email_address = None;
for _ in 0..3 {
// NB: We try a few times to make sure we don't hit an email/username collision.
let user_email_address = generate_random_synthetic_email();
let user_email_gravatar_hash = email_to_gravatar(&user_email_address);
let display_name = generate_random_username();
let username = display_name.trim().to_lowercase();
info!("generated username: {} ; generated email: {}", username, user_email_address);
let result = create_account_from_stripe_checkout(
CreateAccountFromStripeCheckoutArgs {
username: &username,
display_name: &display_name,
email_address: &user_email_address,
email_gravatar_hash: &user_email_gravatar_hash,
maybe_feature_flags: Some(&user_feature_flags),
ip_address: &ip_address,
maybe_source,
},
Transactor::for_transaction(&mut transaction),
).await;
match result {
Ok(token) => {
final_user_token = Some(token);
final_username = Some(username);
final_display_name = Some(display_name);
final_email_address = Some(user_email_address);
break;
},
Err(CreateAccountError::UsernameIsTaken) => {
continue; // NB: We'll try again with a new username.
},
Err(err) => {
warn!("error creating account from google sso: {:?}", err);
return Err(CommonWebError::ServerError);
},
}
}
let final_user_token = final_user_token.ok_or_else(|| {
error!("Could not generate user token after several attempts");
CommonWebError::ServerError
})?;
let final_username = final_username.ok_or_else(|| {
error!("Could not generate username after several attempts");
CommonWebError::ServerError
})?;
let final_display_name = final_display_name.ok_or_else(|| {
error!("Could not generate display name after several attempts");
CommonWebError::ServerError
})?;
let final_email_address = final_email_address.ok_or_else(|| {
error!("Could not generate email address after several attempts");
CommonWebError::ServerError
})?;
transaction.commit()
.await
.map_err(|e| {
warn!("Could not commit transaction: {:?}", e);
CommonWebError::ServerError
})?;
let session_token = create_user_session_with_transactor(
&final_user_token,
&ip_address,
Transactor::for_connection(mysql_connection))
.await
.map_err(|e| {
warn!("error creating user session: {:?}", e);
CommonWebError::ServerError
})?;
let success_url = stripe_config.checkout_success_url.clone();
let cancel_url = stripe_config.checkout_cancel_url.clone();
let checkout_session = {
// `client_reference_id`
// Stripe Docs:
// A unique string to reference the Checkout Session.
// This can be a customer ID, a cart ID, or similar, and can be used to reconcile the session
// with your internal systems.
//
// Our Notes:
// This gets reported back in the Checkout Session (and related webhooks) as
// `client_reference_id`. Passing the same ID on multiple checkouts does not unify or
// cross-correlate customers and only seems to be metadata for the checkout session itself.
// This is probably only useful for tracking checkout session engagement.
//params.client_reference_id = Some("SOME_INTERNAL_ID");
// `customer_email`
// Stripe Docs:
// If provided, this value will be used when the Customer object is created. If not provided,
// customers will be asked to enter their email address. Use this parameter to prefill
// customer data if you already have an email on file. To access information about the
// customer once a session is complete, use the customer field.
//
// Our Notes:
// This does not look up previous customers with the same email and will not unify or
// cross-correlate customers. By default the field will be un-editable in the checkout flow
// if this is specified.
//params.customer_email = Some("email@example.com");
let mut metadata = HashMap::new();
// NB: We don't insert email or username as these are synthetic.
metadata.insert(STRIPE_ARTCRAFT_METADATA_USER_TOKEN.to_string(), final_user_token.to_string());
let mut checkout_builder = CreateCheckoutSession::new()
.success_url(&success_url)
.cancel_url(&cancel_url)
.mode(CheckoutSessionMode::Subscription)
.line_items(vec![
CreateCheckoutSessionLineItems {
price: Some(price_id.to_string()),
quantity: Some(1),
..Default::default()
}
])
.saved_payment_method_options(CreateCheckoutSessionSavedPaymentMethodOptions {
allow_redisplay_filters: Some(vec![
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Always,
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Limited,
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Unspecified,
]),
// The user can choose to tick a checkbox that saves their card for redisplay later.
payment_method_save: Some(CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave::Enabled),
})
.allow_promotion_codes(true) // Allow promo codes / coupons
.automatic_tax(CreateCheckoutSessionAutomaticTax {
enabled: true, // This will ask for the customer's location
liability: None,
})
.metadata(metadata.clone())
.subscription_data(CreateCheckoutSessionSubscriptionData {
metadata: Some(metadata),
..Default::default()
})
;
let checkout_session = checkout_builder
.send(&stripe_config.client)
.await
.map_err(|err| {
error!("Stripe Error: {:?}", err);
CommonWebError::ServerError
})?;
checkout_session
};
Ok(CreationPayload {
checkout_session,
maybe_user_metadata: Some(UserMetadata {
user_token: final_user_token,
session_token,
username: final_username,
display_name: final_display_name,
email_address: final_email_address,
})
})
}
@@ -0,0 +1,167 @@
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
use crate::endpoints::checkout_with_user_signup::creation_payload::CreationPayload;
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use crate::utils::common_web_error::CommonWebError;
use actix_web::web::Data;
use component_traits::traits::internal_user_lookup::UserMetadata;
use enums::common::payments_namespace::PaymentsNamespace;
use log::{error, info, warn};
use mysql_queries::queries::users::user_stripe_customer_links::find_user_stripe_customer_link::find_user_stripe_customer_link_using_connection;
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use std::collections::HashMap;
use std::str::FromStr;
use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData};
use stripe_shared::{CheckoutSession, CheckoutSessionMode, CustomerId, PriceId};
pub (super) async fn user_exists_case(
price_id: &str,
user_metadata: &UserMetadata,
mysql_connection: &mut PoolConnection<MySql>,
stripe_config: &ArtcraftStripeConfigWithClient,
) -> Result<CreationPayload, CommonWebError> {
// NB: Currently the stripe customer id field in the `users` table is only for FakeYou subscriptions,
// so we need to look up any existing Artcraft subscription separately. This is needed to pre-fill
// the Stripe billing form.
let maybe_active_subscription = find_subscription_for_owner_user_using_connection(
&user_metadata.user_token_typed,
PaymentsNamespace::Artcraft,
mysql_connection
).await.map_err(|err| {
error!("Error looking up user's ({}) existing subscription: {:?}", &user_metadata.user_token_typed, err);
CommonWebError::ServerError // NB: This was probably *our* fault.
})?;
if maybe_active_subscription.is_some() {
return Err(CommonWebError::BadInputWithSimpleMessage(
"user already has an active subscription plan; use the portal to update the plan"
.to_string()))
}
// NB: This works, but it feels really weird to have the customer's old email shown in a
// completely immutable state in the stripe checkout form.
//
// let maybe_inactive_subscription = find_possibly_inactive_first_subscription_for_owner_user_using_connection(
// &user_metadata.user_token_typed,
// PaymentsNamespace::Artcraft,
// &mut mysql_connection
// ).await.map_err(|err| {
// error!("Error looking up user's ({}) possibly existing inactive subscription: {:?}",
// &user_metadata.user_token_typed, err);
// CommonWebError::ServerError // NB: This was probably *our* fault.
// })?;
// let maybe_existing_inactive_stripe_customer_id = maybe_inactive_subscription.as_ref()
// .map(|sub| sub.stripe_customer_id.as_str());
let mut maybe_existing_stripe_customer_id = None;
let result = find_user_stripe_customer_link_using_connection(
&user_metadata.user_token_typed,
PaymentsNamespace::Artcraft,
mysql_connection
).await;
// NB: Fail silently.
if let Ok(Some(link)) = result {
maybe_existing_stripe_customer_id = CustomerId::from_str(&link.stripe_customer_id).ok();
} else if let Err(err) = result {
warn!("Error looking up user's ({}) existing stripe customer link: {:?}", &user_metadata.user_token_typed, err);
}
let success_url = stripe_config.checkout_success_url.clone();
let cancel_url = stripe_config.checkout_cancel_url.clone();
let checkout_session = {
// `client_reference_id`
// Stripe Docs:
// A unique string to reference the Checkout Session.
// This can be a customer ID, a cart ID, or similar, and can be used to reconcile the session
// with your internal systems.
//
// Our Notes:
// This gets reported back in the Checkout Session (and related webhooks) as
// `client_reference_id`. Passing the same ID on multiple checkouts does not unify or
// cross-correlate customers and only seems to be metadata for the checkout session itself.
// This is probably only useful for tracking checkout session engagement.
//params.client_reference_id = Some("SOME_INTERNAL_ID");
// `customer_email`
// Stripe Docs:
// If provided, this value will be used when the Customer object is created. If not provided,
// customers will be asked to enter their email address. Use this parameter to prefill
// customer data if you already have an email on file. To access information about the
// customer once a session is complete, use the customer field.
//
// Our Notes:
// This does not look up previous customers with the same email and will not unify or
// cross-correlate customers. By default the field will be un-editable in the checkout flow
// if this is specified.
//params.customer_email = Some("email@example.com");
let mut metadata = HashMap::new();
metadata.insert(STRIPE_ARTCRAFT_METADATA_USER_TOKEN.to_string(), user_metadata.user_token.to_string());
if let Some(username) = user_metadata.username.as_deref() {
metadata.insert(STRIPE_ARTCRAFT_METADATA_USERNAME.to_string(), username.to_string());
}
if let Some(user_email) = user_metadata.user_email.as_deref() {
metadata.insert(STRIPE_ARTCRAFT_METADATA_EMAIL.to_string(), user_email.to_string());
}
let mut checkout_builder = CreateCheckoutSession::new()
.success_url(&success_url)
.cancel_url(&cancel_url)
.mode(CheckoutSessionMode::Subscription)
.line_items(vec![
CreateCheckoutSessionLineItems {
price: Some(price_id.to_string()),
quantity: Some(1),
..Default::default()
}
])
.saved_payment_method_options(CreateCheckoutSessionSavedPaymentMethodOptions {
allow_redisplay_filters: Some(vec![
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Always,
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Limited,
CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Unspecified,
]),
// The user can choose to tick a checkbox that saves their card for redisplay later.
payment_method_save: Some(CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave::Enabled),
})
.allow_promotion_codes(true) // Allow promo codes / coupons
.automatic_tax(CreateCheckoutSessionAutomaticTax {
enabled: true, // This will ask for the customer's location
liability: None,
})
.metadata(metadata.clone())
.subscription_data(CreateCheckoutSessionSubscriptionData {
metadata: Some(metadata),
..Default::default()
})
;
if let Some(customer_id) = maybe_existing_stripe_customer_id {
info!("Adding existing stripe customer id to checkout session: {}", customer_id.as_str());
checkout_builder = checkout_builder.customer(customer_id);
}
let checkout_session = checkout_builder
.send(&stripe_config.client)
.await
.map_err(|err| {
error!("Stripe Error: {:?}", err);
CommonWebError::ServerError
})?;
checkout_session
};
Ok(CreationPayload {
checkout_session,
maybe_user_metadata: None,
})
}
@@ -1,3 +1,4 @@
pub mod checkout;
pub mod checkout_with_user_signup;
pub mod customer_portal;
pub mod webhook;
@@ -32,17 +32,18 @@ path = "src/api_doc_cli.rs"
workspace-build-acceleration.workspace = true
# Internal
actix_artcraft.workspace = true
actix_cors_configs = { path = "../../../lib/actix_cors_configs" }
actix_helpers = { path = "../../../lib/actix_helpers" }
actix_helpers.workspace = true
artcraft_api_defs.workspace = true
bootstrap = { path = "../../../lib/bootstrap" }
bucket_paths = { path = "../../../lib/bucket_paths" }
cloud_storage.workspace = true
collections = { path = "../../../lib/data/collections" }
collections.workspace = true
component_traits.workspace = true
composite_identifiers = { path = "../../../schema/public/composite_identifiers" }
config = { path = "../../../lib/deprecated/config" }
cookies = { path = "../../../lib/cookies" }
cookies.workspace = true
crockford = { path = "../../../lib/data/crockford" }
datetimes = { path = "../../../lib/data/datetimes" }
easyenv = { path = "../../../lib/easyenv" }
@@ -78,6 +79,7 @@ tokens.workspace = true
tts_common = { path = "../../../lib/tts_common" }
url_config = { path = "../../../lib/url_config" }
user_input_common = { path = "../../../lib/user_input_common" }
users.workspace = true
videos = { path = "../../../lib/files/videos" }
# Internal Components
@@ -7,13 +7,12 @@ use std::collections::HashMap;
use std::fmt;
use std::fmt::Formatter;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source_enum;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::validations::is_reserved_username::is_reserved_username;
use crate::http_server::validations::validate_passwords::validate_passwords;
use crate::http_server::validations::validate_username::validate_username;
use crate::util::email_to_gravatar::email_to_gravatar;
use crate::util::enroll_in_studio::enroll_in_studio;
use actix_artcraft::requests::get_request_signup_source_enum::get_request_signup_source_enum;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
@@ -29,6 +28,7 @@ use password::bcrypt_hash_password::bcrypt_hash_password;
use sqlx::MySqlPool;
use tokens::tokens::user_sessions::UserSessionToken;
use user_input_common::check_for_slurs::contains_slurs;
use users::email::email_to_gravatar::email_to_gravatar;
use utoipa::ToSchema;
#[derive(ToSchema, Deserialize)]
@@ -9,8 +9,8 @@ use std::fmt::Formatter;
use crate::http_server::endpoints::users::google_sso::check_claims::check_claims;
use crate::http_server::endpoints::users::google_sso::handle_existing_sso_account::{handle_existing_sso_account, ExistingAccountArgs};
use crate::http_server::endpoints::users::google_sso::handle_new_sso_account::{handle_new_sso_account, NewSsoArgs};
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Json};
@@ -1,9 +1,7 @@
use crate::http_server::endpoints::users::google_sso::google_sso_handler::GoogleCreateAccountErrorResponse;
use crate::http_server::endpoints::users::google_sso::handle_new_sso_account::NewSsoAccountInfo;
use crate::http_server::requests::get_request_signup_source::{get_request_signup_source, get_request_signup_source_enum};
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::util::email_to_gravatar::email_to_gravatar;
use crate::util::generate_random_username::generate_random_username;
use actix_artcraft::requests::get_request_signup_source_enum::get_request_signup_source_enum;
use actix_web::HttpRequest;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use google_sign_in::claims::claims::Claims;
@@ -15,6 +13,8 @@ use mysql_queries::queries::users::user::create::create_account_from_google_sso:
use mysql_queries::utils::transactor::Transactor;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use users::email::email_to_gravatar::email_to_gravatar;
use users::username::generate_random_username::generate_random_username;
pub struct CreateArgs<'a> {
pub http_request: &'a HttpRequest,
@@ -6,22 +6,21 @@
use std::fmt;
use std::fmt::Formatter;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{info, warn};
use sqlx::MySqlPool;
use utoipa::ToSchema;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use log::{info, warn};
use mysql_queries::queries::users::user::get::lookup_user_for_login_by_email::lookup_user_for_login_by_email;
use mysql_queries::queries::users::user::get::lookup_user_for_login_by_username::lookup_user_for_login_by_username;
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use password::bcrypt_confirm_password::bcrypt_confirm_password;
use sqlx::MySqlPool;
use tokens::tokens::user_sessions::UserSessionToken;
use utoipa::ToSchema;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::util::enroll_in_studio::enroll_in_studio;
@@ -5,18 +5,16 @@
use std::fmt;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use log::warn;
use sqlx::MySqlPool;
use utoipa::ToSchema;
use http_server_common::response::response_error_helpers::to_simple_json_error;
use log::warn;
use mysql_queries::queries::users::user_sessions::delete_user_session::delete_user_session;
use sqlx::MySqlPool;
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
pub struct LogoutSuccessResponse {
@@ -1,22 +1,20 @@
use std::fmt::Display;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
use log::{error, warn};
use serde::Deserialize;
use sqlx::MySqlPool;
use strum_macros::Display;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use log::{error, warn};
use mysql_queries::queries::users::user_password_resets::change_password_from_password_reset::{change_password_from_password_reset, ChangePasswordFromPasswordResetArgs};
use mysql_queries::queries::users::user_password_resets::lookup_password_reset_request::lookup_password_reset_request;
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use password::bcrypt_hash_password::bcrypt_hash_password;
use serde::Deserialize;
use sqlx::MySqlPool;
use strum_macros::Display;
use tokens::tokens::user_sessions::UserSessionToken;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
#[derive(Deserialize)]
pub struct PasswordResetRedemptionRequest {
reset_token: String,
@@ -1,3 +1,2 @@
pub mod get_request_domain_branding;
pub mod get_request_signup_source;
pub mod request_headers;
@@ -1,3 +1,2 @@
pub mod http;
pub mod lookup;
pub mod session_checker;
@@ -3,19 +3,18 @@
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_web::HttpRequest;
use log::warn;
use sqlx::pool::PoolConnection;
use sqlx::{Executor, MySql, MySqlPool};
use errors::AnyhowResult;
use log::warn;
use mysql_queries::queries::users::user_sessions::get_user_session_by_token::{get_user_session_by_token, get_user_session_by_token_pooled_connection, SessionUserRecord};
use mysql_queries::queries::users::user_sessions::get_user_session_by_token_light::{get_user_session_by_token_light, SessionRecord};
use mysql_queries::queries::users::user_subscriptions::list_active_user_subscriptions::list_active_user_subscriptions;
use redis_caching::redis_ttl_cache::{RedisTtlCache, RedisTtlCacheConnection};
use redis_common::redis_cache_keys::RedisCacheKeys;
use sqlx::pool::PoolConnection;
use sqlx::{Executor, MySql, MySqlPool};
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::session::lookup::user_session_extended::{UserSessionExtended, UserSessionPreferences, UserSessionPremiumPlanInfo, UserSessionRoleAndPermissions, UserSessionSubscriptionPlan, UserSessionUserDetails};
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
+11 -12
View File
@@ -21,17 +21,7 @@ use std::sync::Arc;
use std::time::Duration;
use actix::Actor;
use actix_multipart::form::MultipartFormConfig;
use actix_web::middleware::{DefaultHeaders, Logger};
use actix_web::{middleware, web, App, HttpServer};
use anyhow::anyhow;
use chrono::Utc;
use elasticsearch::http::transport::Transport;
use elasticsearch::Elasticsearch;
use log::info;
use redis::Client;
use tokio::runtime::Runtime;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_cors_configs::cors::build_cors_config;
use actix_cors_configs::shared_array_buffer_cors::shared_array_buffer_cors;
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_filter::BannedCidrFilter;
@@ -44,24 +34,34 @@ use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoint_filte
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::disabled_endpoints::DisabledEndpoints;
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::exact_match_disabled_endpoints::ExactMatchDisabledEndpoints;
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::prefix_disabled_endpoints::PrefixDisabledEndpoints;
use actix_multipart::form::MultipartFormConfig;
use actix_web::middleware::{DefaultHeaders, Logger};
use actix_web::{middleware, web, App, HttpServer};
use anyhow::anyhow;
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfig;
use billing_component::stripe::stripe_config::{FullUrlOrPath, StripeCheckoutConfigs, StripeConfig, StripeCustomerPortalConfigs, StripeSecrets};
use billing_component::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
use billing_component::stripe::traits::internal_subscription_product_lookup::InternalSubscriptionProductLookup;
use bootstrap::bootstrap::{bootstrap, BootstrapArgs};
use chrono::Utc;
use cloud_storage::bucket_client::BucketClient;
use component_traits::traits::internal_user_lookup::InternalUserLookup;
use config::common_env::CommonEnv;
use config::shared_constants::DEFAULT_RUST_LOG;
use elasticsearch::http::transport::Transport;
use elasticsearch::Elasticsearch;
use email_sender::smtp_email_sender::SmtpEmailSender;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use log::info;
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
use mysql_queries::mediators::badge_granter::BadgeGranter;
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
use redis::Client;
use redis_caching::redis_ttl_cache::RedisTtlCache;
use reusable_types::server_environment::ServerEnvironment;
use tokio::runtime::Runtime;
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
@@ -75,7 +75,6 @@ use crate::configs::static_api_tokens::StaticApiTokenSet;
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
use crate::http_server::middleware::pushback_filter_middleware::PushbackFilter;
use crate::http_server::routes::add_routes::add_routes;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::session::session_checker::SessionChecker;
use crate::http_server::web_utils::handle_multipart_error::handle_multipart_error;
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
@@ -1,13 +1,11 @@
use chrono::{DateTime, Utc};
use elasticsearch::Elasticsearch;
use redis::Client;
use sqlx::MySqlPool;
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet;
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use billing_component::stripe::stripe_config::StripeConfig;
use chrono::{DateTime, Utc};
use cloud_storage::bucket_client::BucketClient;
use elasticsearch::Elasticsearch;
use email_sender::smtp_email_sender::SmtpEmailSender;
use fal_client::creds::fal_api_key::FalApiKey;
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
@@ -19,8 +17,10 @@ use mysql_queries::queries::media_files::list::list_featured_media_files::Featur
use mysql_queries::queries::model_categories::list_categories_query_builder::CategoryList;
use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult;
use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList;
use redis::Client;
use redis_caching::redis_ttl_cache::RedisTtlCache;
use reusable_types::server_environment::ServerEnvironment;
use sqlx::MySqlPool;
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
use crate::configs::app_startup::username_set::UsernameSet;
@@ -32,7 +32,6 @@ use crate::http_server::endpoints::media_files::list::list_featured_media_files_
use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStats;
use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse;
use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::session::session_checker::SessionChecker;
use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter;
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
@@ -5,10 +5,8 @@ pub mod canonicalize_email_for_users_table;
pub mod check_creator_tokens;
pub mod cleaners;
pub mod delete_role_disambiguation;
pub mod email_to_gravatar;
pub mod encrypted_sort_id;
pub mod enroll_in_studio;
pub mod generate_random_username;
pub mod http_download_url_to_bytes;
pub mod http_download_url_to_tempfile;
pub mod lookup;