mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
checkout flow with account creation; endpoints to support (#1025)
New endpoints: - checkout flow with integrated account creation (!!!) - change password - change email New updates: - Session info handler returns onboarding state
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n password_hash = ?,\n ip_address_last_update = ?,\n is_without_password = false,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03761e50f94f2805f1c5c64f598a23af8d708e514a01c6090570b2e0bfabfe2c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n password_hash = ?,\n ip_address_last_update = ?,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "05de463f1c408793d64fb011b1732e3c1b3cf485076b3e1a4a50d16683b99e46"
|
||||
}
|
||||
+94
-50
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_confirmed,\n users.email_gravatar_hash,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
|
||||
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_gravatar_hash,\n \n users.email_confirmed,\n users.email_confirmed_by_google,\n users.email_is_synthetic,\n users.is_without_password,\n users.username_is_not_customized,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -45,16 +45,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email_confirmed",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "email_gravatar_hash",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -64,28 +54,38 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_stripe_customer_id",
|
||||
"ordinal": 5,
|
||||
"name": "email_confirmed",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email_confirmed_by_google",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_loyalty_program_key",
|
||||
"name": "email_is_synthetic",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "disable_gravatar",
|
||||
"name": "is_without_password",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
@@ -95,6 +95,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "username_is_not_customized",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_stripe_customer_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_loyalty_program_key",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "disable_gravatar",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "auto_play_audio_preference",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -104,7 +144,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 14,
|
||||
"name": "auto_play_video_preference",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -114,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 15,
|
||||
"name": "preferred_tts_result_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -124,7 +164,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 16,
|
||||
"name": "preferred_w2l_result_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -134,7 +174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 17,
|
||||
"name": "user_role_slug",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -144,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 18,
|
||||
"name": "is_banned",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -154,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 19,
|
||||
"name": "can_access_studio",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -164,7 +204,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 20,
|
||||
"name": "maybe_feature_flags",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -174,7 +214,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 21,
|
||||
"name": "can_use_tts",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -184,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 22,
|
||||
"name": "can_use_w2l",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -194,7 +234,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 23,
|
||||
"name": "can_delete_own_tts_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -204,7 +244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 24,
|
||||
"name": "can_delete_own_w2l_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -214,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 25,
|
||||
"name": "can_delete_own_account",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -224,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 26,
|
||||
"name": "can_upload_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -234,7 +274,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 27,
|
||||
"name": "can_upload_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -244,7 +284,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 28,
|
||||
"name": "can_delete_own_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -254,7 +294,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 29,
|
||||
"name": "can_delete_own_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -264,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 30,
|
||||
"name": "can_approve_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -274,7 +314,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 31,
|
||||
"name": "can_edit_other_users_profiles",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -284,7 +324,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 32,
|
||||
"name": "can_edit_other_users_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -294,7 +334,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 33,
|
||||
"name": "can_edit_other_users_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -304,7 +344,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"ordinal": 34,
|
||||
"name": "can_delete_other_users_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -314,7 +354,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"ordinal": 35,
|
||||
"name": "can_delete_other_users_tts_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -324,7 +364,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"ordinal": 36,
|
||||
"name": "can_delete_other_users_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -334,7 +374,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"ordinal": 37,
|
||||
"name": "can_delete_other_users_w2l_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -344,7 +384,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 34,
|
||||
"ordinal": 38,
|
||||
"name": "can_ban_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -354,7 +394,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"ordinal": 39,
|
||||
"name": "can_delete_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -374,6 +414,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
@@ -406,5 +450,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7"
|
||||
"hash": "3ca99449c071cb0a8d90a735e1a767a94be224a02e0d16e81997d569af8d4cbb"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\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\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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 19
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "453f0386cfe34e533501ba29dc2022fc449266f9858f16886bde9a0df6b82026"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n email_gravatar_hash = ?,\n ip_address_last_update = ?,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4737c44e7b1c73b21a245dd9d1f96aecfc4c614ed19ac4b590a26809c98fee03"
|
||||
}
|
||||
+12
@@ -0,0 +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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 22
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_gravatar_hash = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6fa90a16784c7b7e3436877c2a751bf0b60054640ebe69cb5f33938238def0fe"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n password_hash = ?,\n ip_address_last_update = ?,\n is_without_password = false,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b8795466b4251d869bdcc644324c6a873791ad1ef0d6d43f77ad2569accec3a4"
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n email_address,\n email_is_synthetic\nFROM users\nWHERE token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email_is_synthetic",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c2dd5120b34d62153772acc94f97c3b7a22b7c53216721e54882c0019d2467e6"
|
||||
}
|
||||
Generated
+35
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN
|
||||
was_eagerly_provisioned;
|
||||
+7
@@ -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.
|
||||
|
||||
@@ -7,4 +7,5 @@ pub mod media_file;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
pub mod subscriptions;
|
||||
pub mod users;
|
||||
pub mod utils;
|
||||
+54
@@ -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;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ChangePasswordRequest {
|
||||
pub password: String,
|
||||
pub password_confirmation: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ChangePasswordResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailRequest {
|
||||
pub email_address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditUsernameRequest {
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditUsernameResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod change_password;
|
||||
pub mod edit_email;
|
||||
pub mod edit_username;
|
||||
pub mod session_info;
|
||||
@@ -0,0 +1,99 @@
|
||||
use crate::common::responses::user_details_light::UserDetailsLight;
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use serde_derive::Serialize;
|
||||
use std::collections::BTreeSet;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FakeYouPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StorytellerStreamPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionUserInfo {
|
||||
pub core_info: UserDetailsLight,
|
||||
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
|
||||
pub onboarding: SessionOnboardingState,
|
||||
|
||||
// TODO(bt,2024-03-30): Remove legacy feature flag
|
||||
#[deprecated(note = "DO NOT USE. Use `maybe_feature_flags` instead! The flag you're looking for is `studio`.")]
|
||||
pub can_access_studio: bool,
|
||||
|
||||
/// Collection of feature / rollout flags
|
||||
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
|
||||
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
|
||||
pub maybe_feature_flags: BTreeSet<UserFeatureFlag>,
|
||||
|
||||
// Premium plans:
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub fakeyou_plan: FakeYouPlan,
|
||||
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub storyteller_stream_plan: StorytellerStreamPlan,
|
||||
|
||||
// Usage permissions:
|
||||
pub can_use_tts: bool,
|
||||
pub can_use_w2l: bool,
|
||||
pub can_delete_own_tts_results: bool,
|
||||
pub can_delete_own_w2l_results: bool,
|
||||
pub can_delete_own_account: bool,
|
||||
|
||||
// Contribution permissions:
|
||||
pub can_upload_tts_models: bool,
|
||||
pub can_upload_w2l_templates: bool,
|
||||
pub can_delete_own_tts_models: bool,
|
||||
pub can_delete_own_w2l_templates: bool,
|
||||
|
||||
// Moderation permissions:
|
||||
pub can_approve_w2l_templates: bool,
|
||||
pub can_edit_other_users_profiles: bool,
|
||||
pub can_edit_other_users_tts_models: bool,
|
||||
pub can_edit_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_tts_models: bool,
|
||||
pub can_delete_other_users_tts_results: bool,
|
||||
pub can_delete_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_w2l_results: bool,
|
||||
pub can_ban_users: bool,
|
||||
pub can_delete_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionOnboardingState {
|
||||
/// If true, the user hasn't set their email.
|
||||
pub email_not_set: bool,
|
||||
|
||||
/// If true, the user hasn't confirmed their email.
|
||||
pub email_not_confirmed: bool,
|
||||
|
||||
/// If true, the user hasn't set their password.
|
||||
pub password_not_set: bool,
|
||||
|
||||
/// If true, the user hasn't set their username (the username they have is random).
|
||||
pub username_not_customized: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionInfoSuccessResponse {
|
||||
pub success: bool,
|
||||
pub logged_in: bool,
|
||||
pub user: Option<SessionUserInfo>,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
-68
@@ -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;
|
||||
+3
-4
@@ -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
-1
@@ -1,3 +1,3 @@
|
||||
pub mod http_user_session_manager;
|
||||
pub mod http_user_session_payload;
|
||||
pub mod payload_signer;
|
||||
pub mod payload_signer;
|
||||
+2
-4
@@ -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();
|
||||
@@ -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)
|
||||
@@ -0,0 +1,34 @@
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
pub fn email_to_gravatar_hash(email_address: &str) -> String {
|
||||
let email = email_address.trim().to_lowercase();
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(email);
|
||||
let hash = hasher.finalize();
|
||||
let gravatar_hash = format!("{:x}", hash);
|
||||
|
||||
gravatar_hash
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
|
||||
#[test]
|
||||
fn test_gravatar() {
|
||||
// Email 1
|
||||
assert_eq!(email_to_gravatar_hash("example@example.com"), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
assert_eq!(email_to_gravatar_hash("EXAMPLE@EXAMPLE.COM"), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" example@example.com \n "), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
|
||||
// Email 2
|
||||
assert_eq!(email_to_gravatar_hash("echelon@gmail.com"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
assert_eq!(email_to_gravatar_hash("ECHELON@GMAIL.COM"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" ECHELON@GMAIL.COM "), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
|
||||
// Misc
|
||||
assert_eq!(email_to_gravatar_hash(""), "d41d8cd98f00b204e9800998ecf8427e".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" "), "d41d8cd98f00b204e9800998ecf8427e".to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use rand::Rng;
|
||||
|
||||
const SYNTHETIC_EMAIL_PREFIX : &str = "synthetic.email";
|
||||
const SYNTHETIC_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!("{}.{}@{}", SYNTHETIC_EMAIL_PREFIX, random_digit, SYNTHETIC_EMAIL_DOMAIN)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::email::generate_random_synthetic_email::generate_random_synthetic_email;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_format() {
|
||||
let email = generate_random_synthetic_email();
|
||||
assert!(email.contains("@"));
|
||||
assert!(email.contains("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod email_to_gravatar_hash;
|
||||
pub mod generate_random_synthetic_email;
|
||||
pub mod validate_email_address_format;
|
||||
@@ -0,0 +1,182 @@
|
||||
|
||||
/// Basic email format validation
|
||||
pub fn validate_email_address_format(email: &str) -> Result<(), String> {
|
||||
let email = email.trim();
|
||||
|
||||
if email.is_empty() {
|
||||
return Err("email address cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if email.len() > 255 {
|
||||
return Err("email address is too long".to_string());
|
||||
}
|
||||
|
||||
// Basic format check: must contain exactly one @ with content on both sides
|
||||
let at_count = email.matches('@').count();
|
||||
if at_count != 1 {
|
||||
return Err("email address must contain exactly one @".to_string());
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = email.split('@').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("invalid email format".to_string());
|
||||
}
|
||||
|
||||
let local = parts[0];
|
||||
let domain = parts[1];
|
||||
|
||||
if local.is_empty() {
|
||||
return Err("email address must have content before @".to_string());
|
||||
}
|
||||
|
||||
if domain.is_empty() {
|
||||
return Err("email address must have a domain after @".to_string());
|
||||
}
|
||||
|
||||
// Domain must contain at least one dot
|
||||
if !domain.contains('.') {
|
||||
return Err("email domain must contain a dot".to_string());
|
||||
}
|
||||
|
||||
// Domain cannot start or end with a dot
|
||||
if domain.starts_with('.') || domain.ends_with('.') {
|
||||
return Err("email domain cannot start or end with a dot".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::validate_email_address_format;
|
||||
|
||||
// Valid emails
|
||||
#[test]
|
||||
fn test_valid_simple_email() {
|
||||
assert!(validate_email_address_format("user@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_subdomain() {
|
||||
assert!(validate_email_address_format("user@mail.example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_plus() {
|
||||
assert!(validate_email_address_format("user+tag@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_dots_in_local() {
|
||||
assert!(validate_email_address_format("first.last@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_numbers() {
|
||||
assert!(validate_email_address_format("user123@example123.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_trimmed() {
|
||||
assert!(validate_email_address_format(" user@example.com ").is_ok());
|
||||
}
|
||||
|
||||
// Empty/whitespace
|
||||
#[test]
|
||||
fn test_invalid_empty() {
|
||||
let result = validate_email_address_format("");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_whitespace_only() {
|
||||
let result = validate_email_address_format(" ");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
// Length
|
||||
#[test]
|
||||
fn test_invalid_too_long() {
|
||||
let long_email = format!("{}@example.com", "a".repeat(250));
|
||||
let result = validate_email_address_format(&long_email);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("too long"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_at_max_length() {
|
||||
// 255 chars is the max
|
||||
let local_part = "a".repeat(242); // 242 + @ + example.com (11) = 254
|
||||
let email = format!("{}@example.com", local_part);
|
||||
assert_eq!(email.len(), 254);
|
||||
assert!(validate_email_address_format(&email).is_ok());
|
||||
}
|
||||
|
||||
// @ symbol
|
||||
#[test]
|
||||
fn test_invalid_no_at() {
|
||||
let result = validate_email_address_format("userexample.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_multiple_at() {
|
||||
let result = validate_email_address_format("user@@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_multiple_at_separated() {
|
||||
let result = validate_email_address_format("user@name@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
// Local part
|
||||
#[test]
|
||||
fn test_invalid_no_local_part() {
|
||||
let result = validate_email_address_format("@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("content before @"));
|
||||
}
|
||||
|
||||
// Domain part
|
||||
#[test]
|
||||
fn test_invalid_no_domain() {
|
||||
let result = validate_email_address_format("user@");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("domain after @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_no_dot() {
|
||||
let result = validate_email_address_format("user@localhost");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("must contain a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_starts_with_dot() {
|
||||
let result = validate_email_address_format("user@.example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_ends_with_dot() {
|
||||
let result = validate_email_address_format("user@example.com.");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_only_dot() {
|
||||
let result = validate_email_address_format("user@.");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
+6
-6
@@ -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() {
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generate_random_username;
|
||||
pub mod constants;
|
||||
+5
@@ -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?;
|
||||
|
||||
+5
@@ -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?;
|
||||
|
||||
+78
@@ -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)
|
||||
}
|
||||
+37
@@ -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;
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
use crate::helpers::transform_optional_result::transform_optional_result;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::fmt::Display;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct MaybeUpdateEmailFromSyntheticValueArgs<'a> {
|
||||
pub user_token: &'a UserToken,
|
||||
pub email_address: &'a str,
|
||||
pub email_gravatar_hash: &'a str,
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
}
|
||||
pub async fn maybe_update_email_from_synthetic_value(
|
||||
args: MaybeUpdateEmailFromSyntheticValueArgs<'_>,
|
||||
) -> AnyhowResult<()> {
|
||||
let result = sqlx::query_as!(
|
||||
UserEmailCheckRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
email_address,
|
||||
email_is_synthetic
|
||||
FROM users
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.user_token.as_str(),
|
||||
)
|
||||
.fetch_one(&mut **args.mysql_connection)
|
||||
.await;
|
||||
|
||||
let maybe_record = transform_optional_result(result)?;
|
||||
|
||||
let record = match maybe_record {
|
||||
Some(record) => record,
|
||||
None => return Err(anyhow!("User not found for token: {}", args.user_token)),
|
||||
};
|
||||
|
||||
let record : UserEmailCheckRecord = record.into();
|
||||
|
||||
if !record.email_is_synthetic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let record_email = record.email_address.to_lowercase();
|
||||
|
||||
// NB: So we don't get the format de-synced, let's just trust the boolean flag.
|
||||
//if !record_email.contains("synthetic") {
|
||||
// return Ok(());
|
||||
//}
|
||||
|
||||
// Normally we don't have much business logic in this crate, but let's be extra careful since we don't have a change log.
|
||||
if record_email.contains("gmail")
|
||||
|| record_email.contains("aol.com")
|
||||
|| record_email.contains("apple")
|
||||
|| record_email.contains("att.net")
|
||||
|| record_email.contains("comcast")
|
||||
|| record_email.contains("earthlink")
|
||||
|| record_email.contains("hotmail")
|
||||
|| record_email.contains("icloud")
|
||||
|| record_email.contains("live.com")
|
||||
|| record_email.contains("mac.com")
|
||||
|| record_email.contains("mail.ru")
|
||||
|| record_email.contains("me.com")
|
||||
|| record_email.contains("outlook")
|
||||
|| record_email.contains("proton")
|
||||
|| record_email.contains("rocketmail")
|
||||
|| record_email.contains("sbcglobal")
|
||||
|| record_email.contains("yahoo")
|
||||
|| record_email.contains("ymail")
|
||||
|| record_email.contains("zoho")
|
||||
{
|
||||
// NB: Just to super extra careful, in case we change the synthetic email format.
|
||||
// This obviously won't catch everything.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let update_email = args.email_address.to_lowercase();
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
email_address = ?,
|
||||
email_gravatar_hash = ?,
|
||||
email_confirmed = false,
|
||||
email_confirmed_by_google = false,
|
||||
email_is_synthetic = false,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
update_email,
|
||||
args.email_gravatar_hash,
|
||||
args.user_token.as_str(),
|
||||
);
|
||||
|
||||
args.mysql_connection.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct UserEmailCheckRecordRaw {
|
||||
email_address: String,
|
||||
email_is_synthetic: i8,
|
||||
}
|
||||
|
||||
struct UserEmailCheckRecord {
|
||||
email_address: String,
|
||||
email_is_synthetic: bool,
|
||||
}
|
||||
|
||||
impl From<UserEmailCheckRecordRaw> for UserEmailCheckRecord {
|
||||
fn from(raw: UserEmailCheckRecordRaw) -> Self {
|
||||
Self {
|
||||
email_address: raw.email_address,
|
||||
email_is_synthetic: i8_to_bool(raw.email_is_synthetic),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
pub mod maybe_update_email_from_synthetic_value;
|
||||
pub mod set_can_access_studio_transactional;
|
||||
pub mod set_user_ban_status;
|
||||
pub mod set_user_feature_flags;
|
||||
pub mod set_user_feature_flags_transactional;
|
||||
pub mod update_email;
|
||||
pub mod update_password;
|
||||
pub mod update_user_record_with_new_stripe_customer_id;
|
||||
pub mod update_username;
|
||||
@@ -0,0 +1,83 @@
|
||||
use crate::utils::transactor::Transactor;
|
||||
use sqlx;
|
||||
use sqlx::Error::Database;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct UpdateEmailArgs<'e, 't> {
|
||||
pub token: &'e UserToken,
|
||||
pub email_address: &'e str,
|
||||
pub email_gravatar_hash: &'e str,
|
||||
pub ip_address: &'e str,
|
||||
pub transactor: Transactor<'e, 't>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateEmailError {
|
||||
EmailIsTaken,
|
||||
DatabaseError { source: sqlx::Error },
|
||||
}
|
||||
|
||||
impl Error for UpdateEmailError {}
|
||||
|
||||
impl Display for UpdateEmailError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UpdateEmailError::EmailIsTaken => {
|
||||
write!(f, "UpdateEmailError: email is taken")
|
||||
}
|
||||
UpdateEmailError::DatabaseError { source } => {
|
||||
write!(f, "UpdateEmailError: database error: {:?}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_email<'e, 't>(
|
||||
args: UpdateEmailArgs<'e, 't>
|
||||
) -> Result<(), UpdateEmailError>
|
||||
{
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
email_address = ?,
|
||||
email_confirmed = false,
|
||||
email_confirmed_by_google = false,
|
||||
email_is_synthetic = false,
|
||||
email_gravatar_hash = ?,
|
||||
ip_address_last_update = ?,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.email_address,
|
||||
args.email_gravatar_hash,
|
||||
args.ip_address,
|
||||
args.token,
|
||||
);
|
||||
|
||||
let query_result = args.transactor.execute(query).await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(Database(err)) => {
|
||||
let maybe_code = err.code()
|
||||
.map(|c| c.into_owned());
|
||||
|
||||
// MySQL error code 23000 is a duplicate key constraint violation
|
||||
if maybe_code.as_deref() == Some("23000")
|
||||
&& err.message().contains("email_address")
|
||||
{
|
||||
return Err(UpdateEmailError::EmailIsTaken);
|
||||
}
|
||||
|
||||
Err(UpdateEmailError::DatabaseError { source: Database(err) })
|
||||
},
|
||||
Err(err) => {
|
||||
Err(UpdateEmailError::DatabaseError { source: err })
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::utils::transactor::Transactor;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx;
|
||||
use std::fmt::Display;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct UpdatePasswordArgs<'e, 't> {
|
||||
pub user_token: &'e UserToken,
|
||||
pub password_hash: &'e str,
|
||||
pub ip_address: &'e str,
|
||||
pub transactor: Transactor<'e, 't>,
|
||||
}
|
||||
|
||||
pub async fn update_password<'e, 't>(
|
||||
args: UpdatePasswordArgs<'e, 't>
|
||||
) -> AnyhowResult<()>
|
||||
{
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
password_hash = ?,
|
||||
ip_address_last_update = ?,
|
||||
is_without_password = false,
|
||||
password_version = password_version + 1,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.password_hash,
|
||||
args.ip_address,
|
||||
args.user_token.as_str(),
|
||||
);
|
||||
|
||||
args.transactor.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+1
@@ -24,6 +24,7 @@ SET
|
||||
email_confirmed = true,
|
||||
password_hash = ?,
|
||||
ip_address_last_update = ?,
|
||||
is_without_password = false,
|
||||
password_version = password_version + 1,
|
||||
version = version + 1
|
||||
WHERE
|
||||
|
||||
+26
-4
@@ -20,8 +20,15 @@ pub struct SessionUserRecord {
|
||||
pub display_name: String,
|
||||
|
||||
pub email_address: String,
|
||||
pub email_confirmed: bool,
|
||||
pub email_gravatar_hash: String,
|
||||
|
||||
// ===== ONBOARDING STATE ===== //
|
||||
|
||||
pub email_confirmed: bool,
|
||||
pub email_confirmed_by_google: bool,
|
||||
pub email_is_synthetic: bool,
|
||||
pub is_without_password: bool,
|
||||
pub username_is_not_customized: bool,
|
||||
|
||||
// ===== PREMIUM FEATURES ===== //
|
||||
|
||||
@@ -111,8 +118,13 @@ SELECT
|
||||
users.display_name,
|
||||
|
||||
users.email_address,
|
||||
users.email_confirmed,
|
||||
users.email_gravatar_hash,
|
||||
|
||||
users.email_confirmed,
|
||||
users.email_confirmed_by_google,
|
||||
users.email_is_synthetic,
|
||||
users.is_without_password,
|
||||
users.username_is_not_customized,
|
||||
|
||||
users.maybe_stripe_customer_id,
|
||||
users.maybe_loyalty_program_key,
|
||||
@@ -173,8 +185,13 @@ WHERE user_sessions.token = ?
|
||||
username: raw_user_record.username,
|
||||
display_name: raw_user_record.display_name,
|
||||
email_address: raw_user_record.email_address,
|
||||
email_confirmed: i8_to_bool(raw_user_record.email_confirmed),
|
||||
email_gravatar_hash: raw_user_record.email_gravatar_hash,
|
||||
// Onboarding
|
||||
email_confirmed: i8_to_bool(raw_user_record.email_confirmed),
|
||||
email_confirmed_by_google: i8_to_bool(raw_user_record.email_confirmed_by_google),
|
||||
email_is_synthetic: i8_to_bool(raw_user_record.email_is_synthetic),
|
||||
is_without_password: i8_to_bool(raw_user_record.is_without_password),
|
||||
username_is_not_customized: i8_to_bool(raw_user_record.username_is_not_customized),
|
||||
// Premium features
|
||||
maybe_stripe_customer_id: raw_user_record.maybe_stripe_customer_id,
|
||||
maybe_loyalty_program_key: raw_user_record.maybe_loyalty_program_key,
|
||||
@@ -239,9 +256,14 @@ struct SessionUserRawDbRecord {
|
||||
display_name: String,
|
||||
|
||||
email_address: String,
|
||||
email_confirmed: i8,
|
||||
email_gravatar_hash: String,
|
||||
|
||||
email_confirmed: i8,
|
||||
email_confirmed_by_google: i8,
|
||||
email_is_synthetic: i8,
|
||||
is_without_password: i8,
|
||||
username_is_not_customized: i8,
|
||||
|
||||
maybe_stripe_customer_id: Option<String>,
|
||||
maybe_loyalty_program_key: Option<String>,
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,30 @@ use utoipa::ToSchema;
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UserSignupSource {
|
||||
#[deprecated(since = "2026-01-30", note = "Use other Artcraft* enum variants instead")]
|
||||
#[serde(rename = "artcraft")]
|
||||
ArtCraft,
|
||||
|
||||
/// ArtCraft Tauri App Onboard Flow
|
||||
#[serde(rename = "artcraft_app")]
|
||||
ArtCraftApp,
|
||||
|
||||
/// artcraft.ai normal onboard flow
|
||||
#[serde(rename = "artcraft_ai_web")]
|
||||
ArtCraftAiWeb,
|
||||
|
||||
/// artcraft.ai stripe checkout flow
|
||||
#[serde(rename = "artcraft_ai_s")]
|
||||
ArtCraftAiStripe,
|
||||
|
||||
/// getartcraft.com normal onboard flow
|
||||
#[serde(rename = "artcraft_get_web")]
|
||||
ArtCraftGetWeb,
|
||||
|
||||
/// getartcraft.com stripe checkout flow
|
||||
#[serde(rename = "artcraft_get_s")]
|
||||
ArtCraftGetStripe,
|
||||
|
||||
#[serde(rename = "fakeyou")]
|
||||
FakeYou,
|
||||
|
||||
@@ -33,6 +54,11 @@ impl UserSignupSource {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ArtCraft => "artcraft",
|
||||
Self::ArtCraftApp => "artcraft_app",
|
||||
Self::ArtCraftAiWeb => "artcraft_ai_web",
|
||||
Self::ArtCraftAiStripe => "artcraft_ai_s",
|
||||
Self::ArtCraftGetWeb => "artcraft_get_web",
|
||||
Self::ArtCraftGetStripe => "artcraft_get_s",
|
||||
Self::FakeYou => "fakeyou",
|
||||
Self::Storyteller => "storyteller",
|
||||
}
|
||||
@@ -41,6 +67,11 @@ impl UserSignupSource {
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"artcraft" => Ok(Self::ArtCraft),
|
||||
"artcraft_app" => Ok(Self::ArtCraftApp),
|
||||
"artcraft_ai_web" => Ok(Self::ArtCraftAiWeb),
|
||||
"artcraft_ai_s" => Ok(Self::ArtCraftAiStripe),
|
||||
"artcraft_get_web" => Ok(Self::ArtCraftGetWeb),
|
||||
"artcraft_get_s" => Ok(Self::ArtCraftGetStripe),
|
||||
"fakeyou" => Ok(Self::FakeYou),
|
||||
"storyteller" => Ok(Self::Storyteller),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
@@ -52,6 +83,11 @@ impl UserSignupSource {
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::ArtCraft,
|
||||
Self::ArtCraftApp,
|
||||
Self::ArtCraftAiWeb,
|
||||
Self::ArtCraftAiStripe,
|
||||
Self::ArtCraftGetWeb,
|
||||
Self::ArtCraftGetStripe,
|
||||
Self::FakeYou,
|
||||
Self::Storyteller,
|
||||
])
|
||||
@@ -69,6 +105,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(UserSignupSource::ArtCraft, "artcraft");
|
||||
assert_serialization(UserSignupSource::ArtCraftApp, "artcraft_app");
|
||||
assert_serialization(UserSignupSource::ArtCraftAiWeb, "artcraft_ai_web");
|
||||
assert_serialization(UserSignupSource::ArtCraftAiStripe, "artcraft_ai_s");
|
||||
assert_serialization(UserSignupSource::ArtCraftGetWeb, "artcraft_get_web");
|
||||
assert_serialization(UserSignupSource::ArtCraftGetStripe, "artcraft_get_s");
|
||||
assert_serialization(UserSignupSource::FakeYou, "fakeyou");
|
||||
assert_serialization(UserSignupSource::Storyteller, "storyteller");
|
||||
}
|
||||
@@ -80,6 +121,10 @@ mod tests {
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(UserSignupSource::ArtCraft.to_str(), "artcraft");
|
||||
assert_eq!(UserSignupSource::ArtCraftApp.to_str(), "artcraft_app");
|
||||
assert_eq!(UserSignupSource::ArtCraftAiWeb.to_str(), "artcraft_ai_web");
|
||||
assert_eq!(UserSignupSource::ArtCraftAiStripe.to_str(), "artcraft_ai_s");
|
||||
assert_eq!(UserSignupSource::ArtCraftGetWeb.to_str(), "artcraft_get_web");
|
||||
assert_eq!(UserSignupSource::FakeYou.to_str(), "fakeyou");
|
||||
assert_eq!(UserSignupSource::Storyteller.to_str(), "storyteller");
|
||||
}
|
||||
@@ -87,6 +132,11 @@ mod tests {
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(UserSignupSource::from_str("artcraft").unwrap(), UserSignupSource::ArtCraft);
|
||||
assert_eq!(UserSignupSource::from_str("artcraft_app").unwrap(), UserSignupSource::ArtCraftApp);
|
||||
assert_eq!(UserSignupSource::from_str("artcraft_ai_web").unwrap(), UserSignupSource::ArtCraftAiWeb);
|
||||
assert_eq!(UserSignupSource::from_str("artcraft_ai_s").unwrap(), UserSignupSource::ArtCraftAiStripe);
|
||||
assert_eq!(UserSignupSource::from_str("artcraft_get_web").unwrap(), UserSignupSource::ArtCraftGetWeb);
|
||||
assert_eq!(UserSignupSource::from_str("artcraft_get_s").unwrap(), UserSignupSource::ArtCraftGetStripe);
|
||||
assert_eq!(UserSignupSource::from_str("fakeyou").unwrap(), UserSignupSource::FakeYou);
|
||||
assert_eq!(UserSignupSource::from_str("storyteller").unwrap(), UserSignupSource::Storyteller);
|
||||
assert!(UserSignupSource::from_str("foo").is_err());
|
||||
@@ -99,8 +149,13 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = UserSignupSource::all_variants();
|
||||
assert_eq!(variants.len(), 3);
|
||||
assert_eq!(variants.len(), 8);
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraft));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraftApp));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraftAiWeb));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraftAiStripe));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraftGetWeb));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraftGetStripe));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::FakeYou));
|
||||
assert_eq!(variants.pop_first(), Some(UserSignupSource::Storyteller));
|
||||
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"] }
|
||||
|
||||
+4
@@ -142,4 +142,8 @@ pub struct SubscriptionPaidEvent {
|
||||
|
||||
/// Token to track in the wallet_ledger_events
|
||||
pub ledger_event_ref: Option<String>,
|
||||
|
||||
/// For eagerly created user records (in the new checkout flow), we need to update the user's email.
|
||||
/// We started creating user accounts without a username, email, and password - so we need to collect it here.
|
||||
pub customer_email: Option<String>,
|
||||
}
|
||||
|
||||
-2
@@ -97,7 +97,5 @@ pub async fn mark_subscription_as_paid(
|
||||
transaction
|
||||
).await?;
|
||||
|
||||
// TODO: Insert wallet ledger entry.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ pub async fn transactionally_fulfill_artcraft_billing_action(
|
||||
ArtcraftBillingAction::SubscriptionPaid(paid_details) => {
|
||||
info!("Completing subscription paid event for user: {:?}", paid_details.owner_user_token);
|
||||
mark_subscription_as_paid(paid_details, transaction).await?;
|
||||
//paid_details.customer_email
|
||||
//paid_details.owner_user_token
|
||||
}
|
||||
// ArtcraftBillingAction::CustomerCreated(customer_link) => {
|
||||
// info!("Linking user to newly created stripe customer: {:?}", customer_link.user_token);
|
||||
|
||||
+16
@@ -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_new_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;
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
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::creation_payload::{CreationPayload, UserMetadata};
|
||||
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 component_traits::traits::internal_user_lookup::InternalUserLookup;
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON;
|
||||
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, PriceId};
|
||||
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
|
||||
|
||||
// /// 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_subscription_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 price_id = PriceId::from_str(&price_id)
|
||||
.map_err(|err| {
|
||||
error!("Error parsing price id: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
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_new_user_metadata {
|
||||
None => {
|
||||
info!("delevering response for existing user...");
|
||||
create_http_response_existing_user(creation_payload.checkout_session)
|
||||
}
|
||||
Some(user_metadata) => {
|
||||
info!("delevering response for new user...");
|
||||
create_http_response_new_user(&session_cookie_manager, creation_payload.checkout_session, user_metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
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 crate::utils::create_checkout::create_subscription_checkout_session::{create_subscription_checkout_session, CreateSubscriptionCheckoutSessionArgs};
|
||||
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::queries::users::user_sessions::create_user_session_with_transactor::create_user_session_with_transactor;
|
||||
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, PriceId};
|
||||
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
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: &PriceId,
|
||||
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_hash(&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 checkout_session = create_subscription_checkout_session(CreateSubscriptionCheckoutSessionArgs {
|
||||
subscription_price_id: price_id,
|
||||
maybe_existing_stripe_customer_id: None,
|
||||
user_token: &final_user_token,
|
||||
username: None,
|
||||
user_email: None,
|
||||
stripe_client: &stripe_config.client,
|
||||
success_url: &stripe_config.checkout_success_url,
|
||||
cancel_url: &stripe_config.checkout_cancel_url,
|
||||
}).await?;
|
||||
|
||||
Ok(CreationPayload {
|
||||
checkout_session,
|
||||
maybe_new_user_metadata: Some(UserMetadata {
|
||||
user_token: final_user_token,
|
||||
session_token,
|
||||
username: final_username,
|
||||
display_name: final_display_name,
|
||||
email_address: final_email_address,
|
||||
})
|
||||
})
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
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 crate::utils::create_checkout::create_subscription_checkout_session::{create_subscription_checkout_session, CreateSubscriptionCheckoutSessionArgs};
|
||||
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: &PriceId,
|
||||
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 checkout_session = create_subscription_checkout_session(CreateSubscriptionCheckoutSessionArgs {
|
||||
subscription_price_id: price_id,
|
||||
maybe_existing_stripe_customer_id: maybe_existing_stripe_customer_id.as_ref(),
|
||||
user_token: &user_metadata.user_token_typed,
|
||||
username: user_metadata.username.as_deref(),
|
||||
user_email: user_metadata.user_email.as_deref(),
|
||||
stripe_client: &stripe_config.client,
|
||||
success_url: &stripe_config.checkout_success_url,
|
||||
cancel_url: &stripe_config.checkout_cancel_url,
|
||||
}).await?;
|
||||
|
||||
Ok(CreationPayload {
|
||||
checkout_session,
|
||||
maybe_new_user_metadata: None,
|
||||
})
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
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::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
|
||||
use crate::stripe_requests::stripe_lookup_subscription_from_subscription_id::stripe_lookup_subscription_from_subscription_id;
|
||||
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use actix_web::web::{Data, Json};
|
||||
@@ -24,7 +25,6 @@ use stripe_checkout::CheckoutSessionMode;
|
||||
use stripe_core::CustomerId;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
|
||||
use crate::requests::lookup_subscription_from_subscription_id::lookup_subscription_from_subscription_id;
|
||||
|
||||
pub async fn stripe_artcraft_customer_portal_switch_plan_handler(
|
||||
http_request: HttpRequest,
|
||||
@@ -130,7 +130,7 @@ async fn update_confirm(
|
||||
let existing_subscription_id = user_subscription.stripe_subscription_id.clone();
|
||||
let existing_product_id = user_subscription.stripe_product_id.clone();
|
||||
|
||||
let existing_subscription = lookup_subscription_from_subscription_id(
|
||||
let existing_subscription = stripe_lookup_subscription_from_subscription_id(
|
||||
&existing_subscription_id,
|
||||
&stripe_config.client
|
||||
).await.map_err(|err| {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
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::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
|
||||
use crate::requests::lookup_subscription_from_subscription_id::lookup_subscription_from_subscription_id;
|
||||
use crate::stripe_requests::stripe_lookup_subscription_from_subscription_id::stripe_lookup_subscription_from_subscription_id;
|
||||
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use actix_web::web::{Data, Json};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod checkout;
|
||||
pub mod checkout_with_user_signup;
|
||||
pub mod customer_portal;
|
||||
pub mod webhook;
|
||||
+55
-6
@@ -1,6 +1,7 @@
|
||||
use actix_web::web::{Bytes, Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
use crate::billing_action_fulfillment::artcraft_billing_action::ArtcraftBillingAction;
|
||||
use crate::billing_action_fulfillment::transactionally_fulfill_artcraft_billing_action::transactionally_fulfill_artcraft_billing_action;
|
||||
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
|
||||
use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
|
||||
@@ -13,13 +14,14 @@ use http_server_common::request::get_request_header_optional::get_request_header
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::billing::stripe::get_stripe_webhook_event_log_by_id::{get_stripe_webhook_event_log_by_id, get_stripe_webhook_event_log_by_id_with_connection};
|
||||
use mysql_queries::queries::billing::stripe::insert_stripe_webhook_event_log::InsertStripeWebhookEventLog;
|
||||
use mysql_queries::queries::users::user::update::maybe_update_email_from_synthetic_value::{maybe_update_email_from_synthetic_value, MaybeUpdateEmailFromSyntheticValueArgs};
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
use serde_derive::Serialize;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Acquire, MySql, MySqlConnection, MySqlPool, Transaction};
|
||||
use std::sync::Arc;
|
||||
use stripe_webhook::{Event, EventObject, Webhook};
|
||||
|
||||
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
/*
|
||||
For one-off payments (eg. credits packs), we see this typical sequence of events:
|
||||
|
||||
@@ -140,7 +142,7 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
let result = process_enriched_event(
|
||||
**server_environment,
|
||||
&stripe_config,
|
||||
enriched_event,
|
||||
&enriched_event,
|
||||
stripe_event_descriptor.clone(),
|
||||
stripe_event_id.clone(),
|
||||
stripe_event_created_at,
|
||||
@@ -164,6 +166,11 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = maybe_update_user_email(&enriched_event, &mut mysql_connection).await {
|
||||
// NB: Don't fail to process the stripe event if we couldn't update the user's email.
|
||||
error!("Error updating user email: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(Json(StripeArtcraftWebhookSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
@@ -172,7 +179,7 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
async fn process_enriched_event(
|
||||
server_environment: ServerEnvironment,
|
||||
stripe_config: &ArtcraftStripeConfigWithClient,
|
||||
artcraft_event: EnrichedWebhookEvent,
|
||||
artcraft_event: &EnrichedWebhookEvent,
|
||||
stripe_event_descriptor: StripeEventDescriptor,
|
||||
stripe_event_id: String,
|
||||
stripe_event_created_at: NaiveDateTime,
|
||||
@@ -197,11 +204,11 @@ async fn process_enriched_event(
|
||||
let query = InsertStripeWebhookEventLog {
|
||||
stripe_event_id,
|
||||
stripe_event_type,
|
||||
maybe_stripe_event_entity_id: artcraft_event.webhook_event_log_summary.maybe_event_entity_id,
|
||||
maybe_stripe_customer_id: artcraft_event.webhook_event_log_summary.maybe_stripe_customer_id,
|
||||
maybe_stripe_event_entity_id: artcraft_event.webhook_event_log_summary.maybe_event_entity_id.clone(),
|
||||
maybe_stripe_customer_id: artcraft_event.webhook_event_log_summary.maybe_stripe_customer_id.clone(),
|
||||
stripe_event_created_at,
|
||||
stripe_is_production,
|
||||
maybe_user_token: artcraft_event.webhook_event_log_summary.maybe_user_token.map(|t| t.to_string()),
|
||||
maybe_user_token: artcraft_event.webhook_event_log_summary.maybe_user_token.as_ref().map(|t| t.to_string()),
|
||||
action_was_taken: artcraft_event.webhook_event_log_summary.action_was_taken,
|
||||
should_ignore_retry: artcraft_event.webhook_event_log_summary.should_ignore_retry,
|
||||
};
|
||||
@@ -216,3 +223,45 @@ async fn process_enriched_event(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For users eagerly created in the new "Stripe user creation flow", we didn't ask for their email, password, or username.
|
||||
/// Stripe will have these details for us. Update if the user hasn't already customized their email yet.
|
||||
async fn maybe_update_user_email(
|
||||
artcraft_event: &EnrichedWebhookEvent,
|
||||
mysql_connection: &mut PoolConnection<MySql>
|
||||
) -> Result<(), StripeArtcraftWebhookError> {
|
||||
|
||||
let action = match artcraft_event.maybe_billing_action.as_ref() {
|
||||
None => return Ok(()),
|
||||
Some(action) => action,
|
||||
};
|
||||
|
||||
let user_token;
|
||||
let user_email;
|
||||
|
||||
match action {
|
||||
ArtcraftBillingAction::SubscriptionPaid(paid) => {
|
||||
user_token = paid.owner_user_token.clone();
|
||||
match paid.customer_email.as_ref() {
|
||||
Some(email) => user_email = email.clone(),
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
|
||||
let user_email = user_email.trim().to_lowercase();
|
||||
let gravatar_hash = email_to_gravatar_hash(&user_email);
|
||||
|
||||
info!("Attempting to update user email from synthetic value for user {} to: {}", &user_token, &user_email);
|
||||
maybe_update_email_from_synthetic_value(MaybeUpdateEmailFromSyntheticValueArgs {
|
||||
user_token: &user_token,
|
||||
email_address: &user_email,
|
||||
email_gravatar_hash: &gravatar_hash,
|
||||
mysql_connection,
|
||||
}).await?;
|
||||
|
||||
info!("Updated user email from synthetic value for user {} to: {}", &user_token, &user_email);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+5
-3
@@ -4,15 +4,16 @@ use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericP
|
||||
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
|
||||
use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
|
||||
use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary;
|
||||
use crate::requests::lookup_subscription_from_subscription_id::lookup_subscription_from_subscription_id;
|
||||
use crate::stripe_requests::stripe_lookup_subscription_from_subscription_id::stripe_lookup_subscription_from_subscription_id;
|
||||
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
|
||||
use crate::utils::expand_ids::expand_subscription_id::expand_subscription_id;
|
||||
use crate::utils::metadata::get_metadata_user_token::get_metadata_user_token;
|
||||
use crate::utils::stripe_event_descriptor::StripeEventDescriptor;
|
||||
use log::{error, info, warn};
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
use stripe::Client;
|
||||
use stripe_shared::{Invoice, InvoiceBillingReason, InvoiceStatus};
|
||||
use crate::utils::stripe_event_descriptor::StripeEventDescriptor;
|
||||
use stripe_types::Expandable;
|
||||
|
||||
// Handle event type: 'invoice.paid'
|
||||
//
|
||||
@@ -101,7 +102,7 @@ pub async fn invoice_paid_extractor(
|
||||
|
||||
info!("Calling Stripe to look up subscription info...");
|
||||
|
||||
let subscription = lookup_subscription_from_subscription_id(
|
||||
let subscription = stripe_lookup_subscription_from_subscription_id(
|
||||
&subscription_id, stripe_client).await?;
|
||||
|
||||
// TODO: Multiple ways to get this; better ways to get this
|
||||
@@ -155,6 +156,7 @@ pub async fn invoice_paid_extractor(
|
||||
maybe_cancel_at: subscription.maybe_cancel_at,
|
||||
maybe_canceled_at: subscription.maybe_canceled_at,
|
||||
ledger_event_ref: Some(ledger_event_ref),
|
||||
customer_email: invoice.customer_email.clone(),
|
||||
})),
|
||||
webhook_event_log_summary: event_log_summary,
|
||||
})
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtc
|
||||
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
|
||||
use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
|
||||
use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary;
|
||||
use crate::requests::lookup_purchase_from_payment_intent_success::lookup_purchase_from_payment_intent_success;
|
||||
use crate::stripe_requests::stripe_lookup_purchase_from_payment_intent_success::stripe_lookup_purchase_from_payment_intent_success;
|
||||
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
|
||||
use crate::utils::metadata::get_metadata_user_token::get_metadata_user_token;
|
||||
use crate::utils::metadata::get_metadata_wallet_token::get_metadata_wallet_token;
|
||||
@@ -72,7 +72,7 @@ pub async fn payment_intent_succeeded_extractor(
|
||||
|
||||
info!("Payment intent succeeded. Looking up payment...");
|
||||
|
||||
let purchase = lookup_purchase_from_payment_intent_success(&payment_intent_id, stripe_client)
|
||||
let purchase = stripe_lookup_purchase_from_payment_intent_success(&payment_intent_id, stripe_client)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error looking up purchase from payment intent {}: {:?}", &payment_intent_id, err);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod billing_action_fulfillment;
|
||||
pub mod configs;
|
||||
pub mod endpoints;
|
||||
pub mod requests;
|
||||
pub mod stripe_requests;
|
||||
pub mod utils;
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod lookup_purchase_from_payment_intent_success;
|
||||
pub mod lookup_subscription_from_subscription_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod stripe_lookup_purchase_from_payment_intent_success;
|
||||
pub mod stripe_lookup_subscription_from_subscription_id;
|
||||
+1
-1
@@ -17,7 +17,7 @@ pub struct PurchaseSummary {
|
||||
}
|
||||
|
||||
// This assumes the payment came from a checkout session.
|
||||
pub async fn lookup_purchase_from_payment_intent_success(
|
||||
pub async fn stripe_lookup_purchase_from_payment_intent_success(
|
||||
payment_intent_id: &str,
|
||||
stripe_client: &Client
|
||||
) -> anyhow::Result<PurchaseSummary> {
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use crate::configs::stripe_client_retry_strategy::STRIPE_CLIENT_RETRY_STRATEGY;
|
||||
use crate::requests::lookup_purchase_from_payment_intent_success::PurchaseSummary;
|
||||
use crate::stripe_requests::stripe_lookup_purchase_from_payment_intent_success::PurchaseSummary;
|
||||
use crate::utils::enum_conversion::recurring_interval_to_reusable_type::recurring_interval_to_reusable_type;
|
||||
use crate::utils::enum_conversion::subscription_status_to_reusable_type::subscription_status_to_reusable_type;
|
||||
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
|
||||
@@ -63,7 +63,7 @@ pub struct SubscriptionAndProduct {
|
||||
pub maybe_canceled_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
pub async fn lookup_subscription_from_subscription_id(
|
||||
pub async fn stripe_lookup_subscription_from_subscription_id(
|
||||
subscription_id: &str,
|
||||
stripe_client: &Client
|
||||
) -> anyhow::Result<SubscriptionAndProduct> {
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN};
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use log::{error, info};
|
||||
use std::collections::HashMap;
|
||||
use stripe::Client;
|
||||
use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData};
|
||||
use stripe_shared::{CheckoutSession, CheckoutSessionMode, CustomerId, PriceId};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct CreateSubscriptionCheckoutSessionArgs<'a> {
|
||||
/// Subscription is required
|
||||
pub subscription_price_id: &'a PriceId,
|
||||
|
||||
/// Optional: Existing user's Stripe customer id, if it exists.
|
||||
/// You MUST look this up.
|
||||
pub maybe_existing_stripe_customer_id: Option<&'a CustomerId>,
|
||||
|
||||
/// User is required
|
||||
pub user_token: &'a UserToken,
|
||||
|
||||
/// Username is optional in the case we eagerly created a user account for the user.
|
||||
pub username: Option<&'a str>,
|
||||
|
||||
/// User email is optional in the case we eagerly created a user account for the user.
|
||||
pub user_email: Option<&'a str>,
|
||||
|
||||
pub stripe_client: &'a Client,
|
||||
|
||||
pub success_url: &'a str,
|
||||
pub cancel_url: &'a str,
|
||||
}
|
||||
|
||||
pub async fn create_subscription_checkout_session(args: CreateSubscriptionCheckoutSessionArgs<'_>) -> Result<CheckoutSession, CommonWebError> {
|
||||
|
||||
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(), args.user_token.to_string());
|
||||
|
||||
if let Some(username) = args.username.as_deref() {
|
||||
metadata.insert(STRIPE_ARTCRAFT_METADATA_USERNAME.to_string(), username.to_string());
|
||||
}
|
||||
|
||||
if let Some(user_email) = args.user_email.as_deref() {
|
||||
metadata.insert(STRIPE_ARTCRAFT_METADATA_EMAIL.to_string(), user_email.to_string());
|
||||
}
|
||||
|
||||
let mut checkout_builder = CreateCheckoutSession::new()
|
||||
.success_url(args.success_url)
|
||||
.cancel_url(args.cancel_url)
|
||||
.mode(CheckoutSessionMode::Subscription)
|
||||
.line_items(vec![
|
||||
CreateCheckoutSessionLineItems {
|
||||
price: Some(args.subscription_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) = args.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(args.stripe_client)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Stripe Error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
checkout_session
|
||||
};
|
||||
|
||||
Ok(checkout_session)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod create_subscription_checkout_session;
|
||||
@@ -1,6 +1,7 @@
|
||||
pub (crate) mod metadata;
|
||||
pub mod artcraft_stripe_config;
|
||||
pub mod common_web_error;
|
||||
pub mod create_checkout;
|
||||
pub mod enum_conversion;
|
||||
pub mod expand_ids;
|
||||
pub mod stripe_event_descriptor;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,7 +102,9 @@ use crate::http_server::endpoints::user_bookmarks::list_user_bookmarks_for_user_
|
||||
use crate::http_server::endpoints::user_ratings::batch_get_user_rating_handler::*;
|
||||
use crate::http_server::endpoints::user_ratings::get_user_rating_handler::*;
|
||||
use crate::http_server::endpoints::user_ratings::set_user_rating_handler::*;
|
||||
use crate::http_server::endpoints::users::change_password_handler::*;
|
||||
use crate::http_server::endpoints::users::create_account_handler::*;
|
||||
use crate::http_server::endpoints::users::edit_email_handler::*;
|
||||
use crate::http_server::endpoints::users::edit_username_handler::*;
|
||||
use crate::http_server::endpoints::users::get_profile_handler::*;
|
||||
use crate::http_server::endpoints::users::google_sso::google_sso_handler::*;
|
||||
@@ -122,6 +124,9 @@ use crate::http_server::endpoints::weights::list::list_weights_by_user_handler::
|
||||
use crate::http_server::endpoints::weights::search::search_model_weights_impl::*;
|
||||
use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*;
|
||||
use crate::http_server::endpoints::weights::update::update_weight_handler::*;
|
||||
use artcraft_api_defs::users::change_password::{ChangePasswordRequest, ChangePasswordResponse};
|
||||
use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse};
|
||||
use artcraft_api_defs::users::edit_username::{EditUsernameRequest, EditUsernameResponse};
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
|
||||
use artcraft_api_defs::common::responses::media_links::*;
|
||||
@@ -313,7 +318,9 @@ use tokens::tokens::zs_voice_datasets::*;
|
||||
crate::http_server::endpoints::user_ratings::batch_get_user_rating_handler::batch_get_user_rating_handler,
|
||||
crate::http_server::endpoints::user_ratings::get_user_rating_handler::get_user_rating_handler,
|
||||
crate::http_server::endpoints::user_ratings::set_user_rating_handler::set_user_rating_handler,
|
||||
crate::http_server::endpoints::users::change_password_handler::change_password_handler,
|
||||
crate::http_server::endpoints::users::create_account_handler::create_account_handler,
|
||||
crate::http_server::endpoints::users::edit_email_handler::edit_email_handler,
|
||||
crate::http_server::endpoints::users::edit_username_handler::edit_username_handler,
|
||||
crate::http_server::endpoints::users::get_profile_handler::get_profile_handler,
|
||||
crate::http_server::endpoints::users::google_sso::google_sso_handler::google_sso_handler,
|
||||
@@ -508,6 +515,12 @@ use tokens::tokens::zs_voice_datasets::*;
|
||||
GptImage1EditImageNumImages,
|
||||
GptImage1EditImageImageQuality,
|
||||
GptImage1EditImageResponse,
|
||||
ChangePasswordError,
|
||||
ChangePasswordRequest,
|
||||
ChangePasswordResponse,
|
||||
EditEmailError,
|
||||
EditEmailRequest,
|
||||
EditEmailResponse,
|
||||
EditUsernameError,
|
||||
EditUsernameRequest,
|
||||
EditUsernameResponse,
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
use utoipa::ToSchema;
|
||||
use artcraft_api_defs::users::change_password::{ChangePasswordRequest, ChangePasswordResponse};
|
||||
use crate::http_server::validations::validate_passwords::validate_passwords;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::users::user::update::update_password::{update_password, UpdatePasswordArgs};
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use password::bcrypt_hash_password::bcrypt_hash_password;
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ChangePasswordError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ChangePasswordError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ChangePasswordError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
ChangePasswordError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
ChangePasswordError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ChangePasswordError::BadInput(reason) => reason.to_string(),
|
||||
ChangePasswordError::NotAuthorized => "unauthorized".to_string(),
|
||||
ChangePasswordError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for ChangePasswordError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Change password for the current user.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Users",
|
||||
path = "/v1/user/change_password",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ChangePasswordResponse),
|
||||
(status = 400, description = "Bad input", body = ChangePasswordError),
|
||||
(status = 401, description = "Not authorized", body = ChangePasswordError),
|
||||
(status = 500, description = "Server error", body = ChangePasswordError),
|
||||
),
|
||||
params(
|
||||
("request" = ChangePasswordRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn change_password_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ChangePasswordRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ChangePasswordResponse>, ChangePasswordError>
|
||||
{
|
||||
let password = request.password.trim();
|
||||
let password_confirmation = request.password_confirmation.trim();
|
||||
|
||||
if let Err(reason) = validate_passwords(password, password_confirmation) {
|
||||
return Err(ChangePasswordError::BadInput(reason));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
ChangePasswordError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|_err| {
|
||||
ChangePasswordError::NotAuthorized
|
||||
})?;
|
||||
|
||||
if user_session.role.is_banned {
|
||||
return Err(ChangePasswordError::NotAuthorized);
|
||||
}
|
||||
|
||||
let password_hash = match bcrypt_hash_password(password.to_string()) {
|
||||
Ok(hash) => hash,
|
||||
Err(err) => {
|
||||
warn!("Bcrypt error: {:?}", err);
|
||||
return Err(ChangePasswordError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let result = update_password(UpdatePasswordArgs {
|
||||
user_token: &user_session.user_token_typed,
|
||||
password_hash: &password_hash,
|
||||
ip_address: &ip_address,
|
||||
transactor: Transactor::for_connection(&mut mysql_connection),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(()) => {},
|
||||
Err(err) => {
|
||||
warn!("Error updating password: {:?}", err);
|
||||
return Err(ChangePasswordError::ServerError);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(ChangePasswordResponse { success: true }))
|
||||
}
|
||||
+4
-4
@@ -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_hash::email_to_gravatar_hash;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(ToSchema, Deserialize)]
|
||||
@@ -185,7 +185,7 @@ pub async fn create_account_handler(
|
||||
|
||||
let email_address = request.email_address.trim().to_lowercase();
|
||||
|
||||
let email_gravatar_hash = email_to_gravatar(&email_address);
|
||||
let email_gravatar_hash = email_to_gravatar_hash(&email_address);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
use utoipa::ToSchema;
|
||||
use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse};
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::users::user::update::update_email::{update_email, UpdateEmailArgs, UpdateEmailError};
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
use users::email::validate_email_address_format::validate_email_address_format;
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum EditEmailError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for EditEmailError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EditEmailError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EditEmailError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EditEmailError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
EditEmailError::BadInput(reason) => reason.to_string(),
|
||||
EditEmailError::NotAuthorized => "unauthorized".to_string(),
|
||||
EditEmailError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for EditEmailError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Edit email address of the current user.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Users",
|
||||
path = "/v1/user/edit_email",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = EditEmailResponse),
|
||||
(status = 400, description = "Bad input", body = EditEmailError),
|
||||
(status = 401, description = "Not authorized", body = EditEmailError),
|
||||
(status = 500, description = "Server error", body = EditEmailError),
|
||||
),
|
||||
params(
|
||||
("request" = EditEmailRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn edit_email_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<EditEmailRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<EditEmailResponse>, EditEmailError>
|
||||
{
|
||||
let email_address = request.email_address.trim().to_lowercase();
|
||||
|
||||
if let Err(reason) = validate_email_address_format(&email_address) {
|
||||
return Err(EditEmailError::BadInput(format!("bad email: {}", &reason)));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
EditEmailError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|_err| {
|
||||
EditEmailError::NotAuthorized
|
||||
})?;
|
||||
|
||||
if user_session.role.is_banned {
|
||||
return Err(EditEmailError::NotAuthorized);
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let gravatar_hash = email_to_gravatar_hash(&email_address);
|
||||
|
||||
let result = update_email(UpdateEmailArgs {
|
||||
token: &user_session.user_token_typed,
|
||||
email_address: &email_address,
|
||||
email_gravatar_hash: &gravatar_hash,
|
||||
ip_address: &ip_address,
|
||||
transactor: Transactor::for_connection(&mut mysql_connection),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(()) => {},
|
||||
Err(UpdateEmailError::EmailIsTaken) => {
|
||||
return Err(EditEmailError::BadInput("email address is already in use".to_string()));
|
||||
}
|
||||
Err(UpdateEmailError::DatabaseError { source }) => {
|
||||
warn!("Error updating email: {:?}", source);
|
||||
return Err(EditEmailError::ServerError);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(EditEmailResponse { success: true }))
|
||||
}
|
||||
+1
-10
@@ -12,7 +12,7 @@ use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use artcraft_api_defs::users::edit_username::{EditUsernameRequest, EditUsernameResponse};
|
||||
use crate::http_server::validations::is_reserved_username::is_reserved_username;
|
||||
use crate::http_server::validations::validate_username::validate_username;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
@@ -23,15 +23,6 @@ use mysql_queries::queries::users::user::update::update_username::{update_userna
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct EditUsernameRequest {
|
||||
display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct EditUsernameResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum EditUsernameError {
|
||||
|
||||
+1
-1
@@ -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};
|
||||
|
||||
+4
-4
@@ -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_hash::email_to_gravatar_hash;
|
||||
use users::username::generate_random_username::generate_random_username;
|
||||
|
||||
pub struct CreateArgs<'a> {
|
||||
pub http_request: &'a HttpRequest,
|
||||
@@ -40,7 +40,7 @@ pub async fn handle_new_sso_account_for_new_user(
|
||||
let user_feature_flags = studio_feature_flags();
|
||||
|
||||
let ip_address = get_request_ip(&args.http_request);
|
||||
let user_email_gravatar_hash = email_to_gravatar(&args.user_email_address);
|
||||
let user_email_gravatar_hash = email_to_gravatar_hash(&args.user_email_address);
|
||||
|
||||
let mut maybe_source = get_request_signup_source_enum(&args.http_request);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+4
-6
@@ -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,4 +1,6 @@
|
||||
pub mod change_password_handler;
|
||||
pub mod create_account_handler;
|
||||
pub mod edit_email_handler;
|
||||
pub mod edit_profile_handler;
|
||||
pub mod edit_username_handler;
|
||||
pub mod get_profile_handler;
|
||||
|
||||
+5
-7
@@ -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,
|
||||
|
||||
+9
-79
@@ -3,7 +3,6 @@
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
@@ -13,90 +12,15 @@ use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use artcraft_api_defs::users::session_info::{FakeYouPlan, SessionInfoSuccessResponse, SessionOnboardingState, SessionUserInfo, StorytellerStreamPlan};
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use http_server_common::response::response_error_helpers::to_simple_json_error;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
|
||||
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FakeYouPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StorytellerStreamPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionUserInfo {
|
||||
pub core_info: UserDetailsLight,
|
||||
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
|
||||
// TODO(bt,2024-03-30): Remove legacy feature flag
|
||||
#[deprecated(note = "DO NOT USE. Use `maybe_feature_flags` instead! The flag you're looking for is `studio`.")]
|
||||
pub can_access_studio: bool,
|
||||
|
||||
/// Collection of feature / rollout flags
|
||||
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
|
||||
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
|
||||
pub maybe_feature_flags: BTreeSet<UserFeatureFlag>,
|
||||
|
||||
// Premium plans:
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub fakeyou_plan: FakeYouPlan,
|
||||
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub storyteller_stream_plan: StorytellerStreamPlan,
|
||||
|
||||
// Usage permissions:
|
||||
pub can_use_tts: bool,
|
||||
pub can_use_w2l: bool,
|
||||
pub can_delete_own_tts_results: bool,
|
||||
pub can_delete_own_w2l_results: bool,
|
||||
pub can_delete_own_account: bool,
|
||||
|
||||
// Contribution permissions:
|
||||
pub can_upload_tts_models: bool,
|
||||
pub can_upload_w2l_templates: bool,
|
||||
pub can_delete_own_tts_models: bool,
|
||||
pub can_delete_own_w2l_templates: bool,
|
||||
|
||||
// Moderation permissions:
|
||||
pub can_approve_w2l_templates: bool,
|
||||
pub can_edit_other_users_profiles: bool,
|
||||
pub can_edit_other_users_tts_models: bool,
|
||||
pub can_edit_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_tts_models: bool,
|
||||
pub can_delete_other_users_tts_results: bool,
|
||||
pub can_delete_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_w2l_results: bool,
|
||||
pub can_ban_users: bool,
|
||||
pub can_delete_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionInfoSuccessResponse {
|
||||
pub success: bool,
|
||||
pub logged_in: bool,
|
||||
pub user: Option<SessionUserInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum SessionInfoError {
|
||||
@@ -170,12 +94,18 @@ pub async fn session_info_handler(
|
||||
// NB: Banned users can't be logged in
|
||||
logged_in = true;
|
||||
user_info = Some(SessionUserInfo {
|
||||
core_info: UserDetailsLight::from_db_fields(
|
||||
core_info: UserDetailsLightBuilder::from_db_fields(
|
||||
&session_data.user_token,
|
||||
&session_data.username,
|
||||
&session_data.display_name,
|
||||
&session_data.email_gravatar_hash,
|
||||
),
|
||||
onboarding: SessionOnboardingState {
|
||||
email_not_set: session_data.email_is_synthetic,
|
||||
email_not_confirmed: !(session_data.email_confirmed || session_data.email_confirmed_by_google),
|
||||
password_not_set: session_data.is_without_password,
|
||||
username_not_customized: session_data.username_is_not_customized,
|
||||
},
|
||||
user_token: session_data.user_token,
|
||||
username: session_data.username.to_string(),
|
||||
display_name: session_data.display_name.to_string(),
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod get_request_domain_branding;
|
||||
pub mod get_request_signup_source;
|
||||
pub mod request_headers;
|
||||
+5
@@ -4,6 +4,7 @@ use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_credits_pack_checkout_handler::stripe_artcraft_create_credits_pack_checkout_handler;
|
||||
use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_subscription_checkout_handler::stripe_artcraft_create_subscription_session_handler;
|
||||
use billing_artcraft_component::endpoints::checkout_with_user_signup::stripe_artcraft_create_subscription_checkout_with_user_signup_handler::stripe_artcraft_create_subscription_checkout_with_user_signup_handler;
|
||||
use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_cancel_plan_handler::stripe_artcraft_customer_portal_cancel_plan_handler;
|
||||
use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_manage_plan_handler::stripe_artcraft_customer_portal_manage_plan_handler;
|
||||
use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_switch_plan_handler::stripe_artcraft_customer_portal_switch_plan_handler;
|
||||
@@ -50,5 +51,9 @@ where
|
||||
.route(web::post().to(stripe_artcraft_customer_portal_update_payment_method_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/user_signup_subscription_checkout")
|
||||
.route(web::post().to(stripe_artcraft_create_subscription_checkout_with_user_signup_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+12
@@ -8,7 +8,9 @@ use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_inference_resul
|
||||
use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler;
|
||||
use crate::http_server::endpoints::tts::list_user_tts_inference_results::list_user_tts_inference_results_handler;
|
||||
use crate::http_server::endpoints::tts::list_user_tts_models::list_user_tts_models_handler;
|
||||
use crate::http_server::endpoints::users::change_password_handler::change_password_handler;
|
||||
use crate::http_server::endpoints::users::create_account_handler::create_account_handler;
|
||||
use crate::http_server::endpoints::users::edit_email_handler::edit_email_handler;
|
||||
use crate::http_server::endpoints::users::edit_profile_handler::edit_profile_handler;
|
||||
use crate::http_server::endpoints::users::edit_username_handler::edit_username_handler;
|
||||
use crate::http_server::endpoints::users::get_profile_handler::get_profile_handler;
|
||||
@@ -99,6 +101,16 @@ pub fn add_user_routes<T, B> (app: App<T>) -> App<T>
|
||||
|
||||
// NB(bt): Modern user profile routes
|
||||
.service(web::scope("/v1/user")
|
||||
.service(
|
||||
web::resource("/change_password")
|
||||
.route(web::post().to(change_password_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/edit_email")
|
||||
.route(web::post().to(edit_email_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/edit_username")
|
||||
.route(web::post().to(edit_username_handler))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user