mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
referral program backend (#1509)
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `token: tokens::tokens::users::UserToken`\nFROM users\n WHERE username = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "566a2d56735180e0f17484789d0debd03c68bda20354882e111476bab6132723"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?,\n maybe_landing_url = ?\n ",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?,\n maybe_landing_url = ?,\n\n maybe_referral_partner = ?,\n maybe_referral_user_token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 24
|
||||
"Right": 26
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3aacc0d270a9073ee5a38595cc32ab7793e0e8b9ce3ad81f554e7198fd0fd509"
|
||||
"hash": "867b22640ae6c49c26973a32d36439933dceef881313a7f3385d61ebe5faeb34"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN maybe_referral_partner;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN maybe_referral_partner VARCHAR(32) DEFAULT NULL;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN maybe_referral_user_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN maybe_referral_user_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -202,9 +202,20 @@ CREATE TABLE users (
|
||||
|
||||
-- ========== TRACKING ==========
|
||||
|
||||
-- Where the user signed up from (NOT NECESSARILY AN ENUM!)
|
||||
-- For now this will be "artcraft", "fakeyou", or "storyteller",
|
||||
-- though we may extend or overload this to handle other cases or metadata.
|
||||
-- Where the user signed up from
|
||||
--
|
||||
-- ArtCraft Values:
|
||||
-- - "artcraft" - (deprecated)
|
||||
-- - "artcraft_app" - Signup with the ArtCraft app
|
||||
-- - "artcraft_get_web" - Signup with ArtCraft website signup flow
|
||||
-- - "artcraft_get_s" - Signup with ArtCraft website Stripe Checkout Flow
|
||||
-- - "artcraft_ai_s" - Signup with Artcraft.ai website Stripe checkout flow
|
||||
-- - "artcraft_get_web" - Signup with ArtCraft.ai website signup flow
|
||||
--
|
||||
-- Other Sources:
|
||||
-- - "fakeyou" - FakeYou signup
|
||||
-- - "storyteller" - Storyteller.ai signup
|
||||
--
|
||||
maybe_source VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- How users created their account
|
||||
@@ -229,6 +240,13 @@ CREATE TABLE users (
|
||||
-- This helps us know what marketing is effective.
|
||||
maybe_landing_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- If a user referred this user, this is either the raw username or raw "referral code"
|
||||
-- that this user used to sign up. This is not verified and can be used for debugging.
|
||||
maybe_referral_partner VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If a user referred this user, we record the user token of the referrer.
|
||||
maybe_referral_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== MODERATION DETAILS ==========
|
||||
|
||||
-- Different than deleted.
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
|
||||
/// Optional: The URL where the user landed when they first arrived, prior to navigation and signing up.
|
||||
/// The browser can send `window.location.href` to the backend so we know how people are finding us.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
|
||||
@@ -64,6 +64,8 @@ async fn seed_user(
|
||||
maybe_source: None,
|
||||
maybe_referral_url: None,
|
||||
maybe_landing_url: None,
|
||||
maybe_referral_partner: None,
|
||||
maybe_referral_user_token: None,
|
||||
maybe_user_token: maybe_user_token.as_ref(),
|
||||
}).await.map_err(|err| anyhow!("err: {:?}", err))?;
|
||||
|
||||
|
||||
+8
@@ -29,6 +29,12 @@ pub struct CreateAccountFromEmailPasswordArgs<'a> {
|
||||
/// We can grab this from `window.location.href`.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the raw username or referral code.
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the user token of the referrer.
|
||||
pub maybe_referral_user_token: Option<&'a UserToken>,
|
||||
|
||||
/// In production code, send this as `None`.
|
||||
/// Only provide an external user token for db integration tests and db seeding tools.
|
||||
/// This allows for knowing the user token a priori.
|
||||
@@ -73,6 +79,8 @@ pub async fn create_account_from_email_and_password(
|
||||
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
maybe_referral_partner: args.maybe_referral_partner,
|
||||
maybe_referral_user_token: args.maybe_referral_user_token,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: args.maybe_user_token,
|
||||
|
||||
+13
-3
@@ -27,6 +27,15 @@ pub struct CreateAccountFromGoogleSsoArgs<'a> {
|
||||
|
||||
/// Comma separated string of feature flags.
|
||||
pub maybe_feature_flags: Option<&'a str>,
|
||||
|
||||
pub maybe_referral_url: Option<String>,
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the raw username or referral code.
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the user token of the referrer.
|
||||
pub maybe_referral_user_token: Option<&'a UserToken>,
|
||||
}
|
||||
|
||||
pub async fn create_account_from_google_sso(
|
||||
@@ -57,9 +66,10 @@ pub async fn create_account_from_google_sso(
|
||||
password_hash: SSO_PASSWORD,
|
||||
is_without_password: true,
|
||||
|
||||
// TODO: Necessary? We're not doing SSO for now.
|
||||
maybe_referral_url: None,
|
||||
maybe_landing_url: None,
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
maybe_referral_partner: args.maybe_referral_partner,
|
||||
maybe_referral_user_token: args.maybe_referral_user_token,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
|
||||
+8
@@ -31,6 +31,12 @@ pub struct CreateAccountFromStripeCheckoutArgs<'a> {
|
||||
|
||||
/// Comma separated string of feature flags.
|
||||
pub maybe_feature_flags: Option<&'a str>,
|
||||
|
||||
/// If a user referred this user, this is the raw username or referral code.
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the user token of the referrer.
|
||||
pub maybe_referral_user_token: Option<&'a UserToken>,
|
||||
}
|
||||
|
||||
pub async fn create_account_from_stripe_checkout(
|
||||
@@ -72,6 +78,8 @@ pub async fn create_account_from_stripe_checkout(
|
||||
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
maybe_referral_partner: args.maybe_referral_partner,
|
||||
maybe_referral_user_token: args.maybe_referral_user_token,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
|
||||
+14
-1
@@ -69,6 +69,13 @@ pub struct GenericCreateAccountArgs<'a> {
|
||||
/// The browser can send `window.location.href` to the backend so we know how people are finding us.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the raw username or referral code.
|
||||
/// Trimmed and truncated to 32 characters.
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
|
||||
/// If a user referred this user, this is the user token of the referrer.
|
||||
pub maybe_referral_user_token: Option<&'a UserToken>,
|
||||
|
||||
/// In production code, send this as `None`.
|
||||
/// Only provide an external user token for db integration tests and db seeding tools.
|
||||
/// This allows for knowing the user token a priori.
|
||||
@@ -139,7 +146,10 @@ SET
|
||||
maybe_signup_method = ?,
|
||||
|
||||
maybe_referral_url = ?,
|
||||
maybe_landing_url = ?
|
||||
maybe_landing_url = ?,
|
||||
|
||||
maybe_referral_partner = ?,
|
||||
maybe_referral_user_token = ?
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
|
||||
@@ -177,6 +187,9 @@ SET
|
||||
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
|
||||
args.maybe_referral_partner,
|
||||
args.maybe_referral_user_token.map(|t| t.as_str()),
|
||||
);
|
||||
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub async fn get_user_token_by_username_with_executor<'e, 'c: 'e, E>(
|
||||
username: &str,
|
||||
mysql_executor: E,
|
||||
) -> Result<Option<UserToken>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let username = username.to_lowercase();
|
||||
|
||||
let result = sqlx::query_as!(
|
||||
UsernameRecord,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: tokens::tokens::users::UserToken`
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
username
|
||||
)
|
||||
.fetch_one(mysql_executor)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(record) => Ok(Some(record.token)),
|
||||
Err(sqlx::Error::RowNotFound) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
struct UsernameRecord {
|
||||
token: UserToken,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod get_user_token_by_username;
|
||||
pub mod get_user_token_by_username_with_executor;
|
||||
pub mod get_username_by_user_token;
|
||||
pub mod lookup_user_for_login_by_email;
|
||||
pub mod lookup_user_for_login_by_email_with_transactor;
|
||||
|
||||
+27
@@ -5,6 +5,7 @@ use crate::endpoints::checkout_with_user_signup::user_creation_case::user_creati
|
||||
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 mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
@@ -92,6 +93,30 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
|
||||
|
||||
let maybe_landing_url = request.maybe_landing_url.clone();
|
||||
|
||||
let maybe_referral_partner = request.maybe_referral_username.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s[..s.len().min(32)].to_string());
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut *mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let maybe_user_metadata = internal_user_lookup
|
||||
.lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
@@ -110,6 +135,8 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
|
||||
&stripe_config,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner,
|
||||
maybe_referral_user_token,
|
||||
).await?
|
||||
},
|
||||
Some(user_metadata) => {
|
||||
|
||||
+5
@@ -21,6 +21,7 @@ use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSes
|
||||
use stripe_shared::{CheckoutSession, CheckoutSessionMode, PriceId};
|
||||
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
use users::email::generate_random_synthetic_email::generate_random_synthetic_email;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use users::username::generate_random_username::generate_random_username;
|
||||
|
||||
pub (super) async fn user_creation_case(
|
||||
@@ -30,6 +31,8 @@ pub (super) async fn user_creation_case(
|
||||
stripe_config: &ArtcraftStripeConfigWithClient,
|
||||
maybe_referral_url: Option<String>,
|
||||
maybe_landing_url: Option<String>,
|
||||
maybe_referral_partner: Option<String>,
|
||||
maybe_referral_user_token: Option<UserToken>,
|
||||
) -> Result<CreationPayload, CommonWebError> {
|
||||
|
||||
let mut transaction = mysql_connection.begin()
|
||||
@@ -86,6 +89,8 @@ pub (super) async fn user_creation_case(
|
||||
maybe_source,
|
||||
maybe_referral_url: maybe_referral_url.clone(),
|
||||
maybe_landing_url: maybe_landing_url.clone(),
|
||||
maybe_referral_partner: maybe_referral_partner.clone(),
|
||||
maybe_referral_user_token: maybe_referral_user_token.as_ref(),
|
||||
},
|
||||
Transactor::for_transaction(&mut transaction),
|
||||
).await;
|
||||
|
||||
+26
@@ -7,7 +7,9 @@ use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::util::cleaners::sanitize_referral_username::sanitize_referral_username;
|
||||
use crate::http_server::validations::is_reserved_username::is_reserved_username;
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use crate::http_server::validations::validate_passwords::validate_passwords;
|
||||
use crate::http_server::validations::validate_username::validate_username;
|
||||
use crate::util::enroll_in_studio::enroll_in_studio;
|
||||
@@ -50,6 +52,9 @@ pub struct CreateAccountRequest {
|
||||
/// Optional: The URL where the user landed when they first arrived, prior to navigation and signing up.
|
||||
/// The browser can send `window.location.href` to the backend so we know how people are finding us.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -213,6 +218,25 @@ pub async fn create_account_handler(
|
||||
|
||||
let maybe_landing_url = request.maybe_landing_url.clone();
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &**mysql_pool).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let create_account_result = create_account_from_email_and_password(
|
||||
&mysql_pool,
|
||||
CreateAccountFromEmailPasswordArgs {
|
||||
@@ -225,6 +249,8 @@ pub async fn create_account_handler(
|
||||
maybe_source,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner: request.maybe_referral_username.as_deref().and_then(sanitize_referral_username),
|
||||
maybe_referral_user_token: maybe_referral_user_token.as_ref(),
|
||||
maybe_user_token: None, // NB: This parameter is for internal testing only
|
||||
}
|
||||
).await;
|
||||
|
||||
+49
@@ -6,6 +6,7 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::util::cleaners::sanitize_referral_username::sanitize_referral_username;
|
||||
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};
|
||||
@@ -19,6 +20,7 @@ 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::google_sign_in_accounts::get_google_sign_in_account_by_subject::get_google_sign_in_account;
|
||||
use mysql_queries::queries::users::user::get::get_user_token_by_username_with_executor::get_user_token_by_username_with_executor;
|
||||
use mysql_queries::queries::users::user_sessions::create_user_session_with_transactor::create_user_session_with_transactor;
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use sqlx::{Acquire, MySqlPool};
|
||||
@@ -79,6 +81,18 @@ use utoipa::ToSchema;
|
||||
#[derive(ToSchema, Deserialize)]
|
||||
pub struct GoogleCreateAccountRequest {
|
||||
pub google_credential: String,
|
||||
|
||||
/// Optional: The referral URL the user arrived from when first hitting the site.
|
||||
/// The browser can send `document.referrer` to the backend.
|
||||
/// If not provided, we'll try the `referer` header.
|
||||
pub maybe_referral_url: Option<String>,
|
||||
|
||||
/// Optional: The URL where the user landed when they first arrived.
|
||||
/// The browser can send `window.location.href`.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// Optional: A referral username or code from a referring user.
|
||||
pub maybe_referral_username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -233,12 +247,47 @@ pub async fn google_sso_handler(
|
||||
username_not_yet_customized = false;
|
||||
},
|
||||
None => {
|
||||
let maybe_referral_url = request.maybe_referral_url.clone()
|
||||
.or_else(|| {
|
||||
http_request.headers().get("referer")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
|
||||
let maybe_landing_url = request.maybe_landing_url.clone();
|
||||
|
||||
let maybe_referral_partner = request.maybe_referral_username.as_deref()
|
||||
.and_then(sanitize_referral_username);
|
||||
|
||||
// Look up referring user by username (optional, fail-open).
|
||||
let maybe_referral_user_token = match request.maybe_referral_username.as_deref() {
|
||||
Some(raw) => {
|
||||
let lookup_username = raw.trim().to_lowercase();
|
||||
if lookup_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match get_user_token_by_username_with_executor(&lookup_username, &mut *mysql_connection).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Referral user lookup failed (continuing): {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let result = handle_new_sso_account(NewSsoArgs {
|
||||
http_request: &http_request,
|
||||
claims,
|
||||
claims_subject: &claims_subject,
|
||||
claims_email_address: &claims_email_address,
|
||||
mysql_connection: &mut mysql_connection,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
maybe_referral_partner,
|
||||
maybe_referral_user_token,
|
||||
}).await?;
|
||||
|
||||
user_token = result.user_token;
|
||||
|
||||
+8
@@ -17,6 +17,10 @@ pub struct NewSsoArgs<'a> {
|
||||
pub claims_subject: &'a str,
|
||||
pub claims_email_address: &'a str,
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
pub maybe_referral_url: Option<String>,
|
||||
pub maybe_landing_url: Option<String>,
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
pub maybe_referral_user_token: Option<UserToken>,
|
||||
}
|
||||
|
||||
pub struct NewSsoAccountInfo {
|
||||
@@ -63,6 +67,10 @@ pub async fn handle_new_sso_account(
|
||||
claims_email_address: &args.claims_email_address,
|
||||
user_email_address: &user_email_address,
|
||||
mysql_connection: args.mysql_connection,
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
maybe_referral_partner: args.maybe_referral_partner,
|
||||
maybe_referral_user_token: args.maybe_referral_user_token,
|
||||
}).await
|
||||
},
|
||||
}
|
||||
|
||||
+9
@@ -14,6 +14,7 @@ use mysql_queries::utils::transactor::Transactor;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Acquire, MySql};
|
||||
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use users::username::generate_random_username::generate_random_username;
|
||||
|
||||
pub struct CreateArgs<'a> {
|
||||
@@ -23,6 +24,10 @@ pub struct CreateArgs<'a> {
|
||||
pub claims_email_address: &'a str,
|
||||
pub user_email_address: &'a str,
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
pub maybe_referral_partner: Option<String>,
|
||||
pub maybe_referral_url: Option<String>,
|
||||
pub maybe_landing_url: Option<String>,
|
||||
pub maybe_referral_user_token: Option<UserToken>,
|
||||
}
|
||||
pub async fn handle_new_sso_account_for_new_user(
|
||||
args: CreateArgs<'_>
|
||||
@@ -64,6 +69,10 @@ pub async fn handle_new_sso_account_for_new_user(
|
||||
maybe_feature_flags: user_feature_flags.as_deref(),
|
||||
ip_address: &ip_address,
|
||||
maybe_source,
|
||||
maybe_referral_partner: args.maybe_referral_partner.clone(),
|
||||
maybe_referral_url: args.maybe_referral_url.clone(),
|
||||
maybe_landing_url: args.maybe_landing_url.clone(),
|
||||
maybe_referral_user_token: args.maybe_referral_user_token.as_ref(),
|
||||
},
|
||||
Transactor::for_transaction(&mut transaction),
|
||||
).await;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod empty_media_file_token_to_null;
|
||||
pub mod empty_media_file_token_to_null;
|
||||
pub mod sanitize_referral_username;
|
||||
@@ -0,0 +1,46 @@
|
||||
const MAX_REFERRAL_PARTNER_LENGTH: usize = 32;
|
||||
|
||||
/// Sanitize a referral username/code for storage
|
||||
/// in the `users.maybe_referral_partner` column.
|
||||
/// Trims whitespace and truncates to 32 characters.
|
||||
/// Returns None if empty after trimming.
|
||||
pub fn sanitize_referral_username(value: &str) -> Option<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let truncated = &trimmed[..trimmed.len().min(MAX_REFERRAL_PARTNER_LENGTH)];
|
||||
Some(truncated.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_returns_none() {
|
||||
assert_eq!(sanitize_referral_username(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_returns_none() {
|
||||
assert_eq!(sanitize_referral_username(" "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace() {
|
||||
assert_eq!(sanitize_referral_username(" alice "), Some("alice".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_long_values() {
|
||||
let long = "a".repeat(50);
|
||||
let result = sanitize_referral_username(&long).unwrap();
|
||||
assert_eq!(result.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_normal_values() {
|
||||
assert_eq!(sanitize_referral_username("bob123"), Some("bob123".to_string()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user