mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 00:18:17 +00:00
new create user account (WIP)
This commit is contained in:
1 parent
47ffa5696a
commit
bf538b55d1
8 files changed
+382
-18
No files matched your search
Generated
+1
@@ -7162,6 +7162,7 @@ dependencies = [
|
||||
"futures 0.3.29",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"google_sign_in",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"http_server_common",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use crate::certs::key_map::KeyMap;
|
||||
use crate::claims::claims::Claims;
|
||||
use crate::claims::google_custom_claims::GoogleCustomClaims;
|
||||
@@ -6,27 +7,10 @@ use errors::{anyhow, AnyhowResult};
|
||||
use jwt_simple::algorithms::RS256PublicKey;
|
||||
use jwt_simple::algorithms::RSAPublicKeyLike;
|
||||
use jwt_simple::common::VerificationOptions;
|
||||
/*
|
||||
Example payload:
|
||||
iss https://accounts.google.com
|
||||
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
sub 113101967612396793777
|
||||
email vocodes2020@gmail.com
|
||||
email_verified true
|
||||
nbf 1726786100
|
||||
name Vocodes Vocodes
|
||||
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
|
||||
given_name Vocodes
|
||||
family_name Vocodes
|
||||
iat 1726786400
|
||||
exp 1726790000
|
||||
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
|
||||
*/
|
||||
|
||||
/// Decode a Google Sign In JWT.
|
||||
/// Verification options can be supplied to increase clock skew tolerance, etc.
|
||||
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
|
||||
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
|
||||
let header = decode_jwt_header(token)?;
|
||||
|
||||
let key_id = header.kid.as_deref()
|
||||
@@ -36,6 +20,16 @@ pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, options: Optio
|
||||
let key = keys.get(key_id)
|
||||
.ok_or_else(|| anyhow!("Key not found"))?;
|
||||
|
||||
if options.is_none() {
|
||||
options = Some(VerificationOptions {
|
||||
allowed_issuers: Some(HashSet::from([
|
||||
"https://accounts.google.com".to_string(),
|
||||
"accounts.google.com".to_string(),
|
||||
])),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
decode_and_verify_token_claims_with_key(key, token, options)
|
||||
}
|
||||
|
||||
@@ -64,6 +58,24 @@ mod tests {
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
/*
|
||||
Example payload:
|
||||
iss https://accounts.google.com
|
||||
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
sub 113101967612396793777
|
||||
email vocodes2020@gmail.com
|
||||
email_verified true
|
||||
nbf 1726786100
|
||||
name Vocodes Vocodes
|
||||
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
|
||||
given_name Vocodes
|
||||
family_name Vocodes
|
||||
iat 1726786400
|
||||
exp 1726790000
|
||||
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let jwk_file = test_file_path("test_data/crypto/google_signin.jwk").unwrap();
|
||||
|
||||
@@ -25,3 +25,5 @@ pub mod download_certs;
|
||||
|
||||
pub(crate) mod jwt;
|
||||
|
||||
// Export for external users
|
||||
pub use jwt_simple::common::VerificationOptions;
|
||||
@@ -45,6 +45,7 @@ enums = { path = "../../../_types/enums" }
|
||||
enums_public = { path = "../../../_types/enums_public" }
|
||||
errors = { path = "../../../lib/errors" }
|
||||
filesys = { path = "../../../lib/files/filesys" }
|
||||
google_sign_in = { path = "../../../lib/api_clients/google_sign_in" }
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
idempotency = { path = "../../../lib/idempotency" }
|
||||
|
||||
@@ -132,6 +132,7 @@ use crate::http_server::endpoints::users::get_profile_handler::*;
|
||||
use crate::http_server::endpoints::users::login_handler::*;
|
||||
use crate::http_server::endpoints::users::logout_handler::*;
|
||||
use crate::http_server::endpoints::users::session_info_handler::*;
|
||||
use crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::*;
|
||||
use crate::http_server::endpoints::users::session_token_info_handler::*;
|
||||
use crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::*;
|
||||
use crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::*;
|
||||
@@ -188,6 +189,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler,
|
||||
crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler,
|
||||
crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler,
|
||||
crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::create_account_from_google_sign_in_handler,
|
||||
crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler,
|
||||
crate::http_server::endpoints::media_files::list::list_media_files_handler::list_media_files_handler,
|
||||
crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::upload_studio_shot_media_file_handler,
|
||||
@@ -348,6 +350,9 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
ByQueueStats,
|
||||
ChangeMediaFileAnimationTypeError,
|
||||
ChangeMediaFileAnimationTypeRequest,
|
||||
GoogleCreateAccountRequest,
|
||||
GoogleCreateAccountErrorResponse,
|
||||
GoogleCreateAccountSuccessResponse,
|
||||
ChangeMediaFileEngineCategoryError,
|
||||
ChangeMediaFileEngineCategoryRequest,
|
||||
ChangeMediaFileVisibilityError,
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
//#![forbid(unused_imports)]
|
||||
//#![forbid(unused_mut)]
|
||||
//#![forbid(unused_variables)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
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 actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
|
||||
use google_sign_in::decode_and_verify_token_claims::decode_and_verify_token_claims;
|
||||
use google_sign_in::download_certs::download_cert_key_set;
|
||||
use google_sign_in::VerificationOptions;
|
||||
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 mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError};
|
||||
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
|
||||
use password::bcrypt_hash_password::bcrypt_hash_password;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
|
||||
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 crate::util::generate_random_username::generate_random_username;
|
||||
|
||||
#[derive(ToSchema, Deserialize)]
|
||||
pub struct GoogleCreateAccountRequest {
|
||||
pub google_credential: String,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
pub struct GoogleCreateAccountSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Tell the user what the automatically created display name was in case
|
||||
/// they want to rename their account.
|
||||
pub automatic_user_display_name: String,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Serialize, Debug)]
|
||||
pub struct GoogleCreateAccountErrorResponse {
|
||||
pub success: bool,
|
||||
pub error_type: GoogleCreateAccountErrorType,
|
||||
pub error_fields: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Copy, Clone, Debug, Serialize)]
|
||||
pub enum GoogleCreateAccountErrorType {
|
||||
BadRequest, // Other request malformed errors, eg. bad Origin header
|
||||
BadInput,
|
||||
EmailTaken,
|
||||
ServerError,
|
||||
UsernameReserved,
|
||||
UsernameTaken,
|
||||
}
|
||||
|
||||
impl GoogleCreateAccountErrorResponse {
|
||||
fn server_error() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: GoogleCreateAccountErrorType::ServerError,
|
||||
error_fields: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bad_request() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: GoogleCreateAccountErrorType::BadRequest,
|
||||
error_fields: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using DeriveMore since Clion doesn't understand it.
|
||||
impl fmt::Display for GoogleCreateAccountErrorResponse {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.error_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for GoogleCreateAccountErrorResponse {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.error_type {
|
||||
GoogleCreateAccountErrorType::BadRequest => StatusCode::BAD_REQUEST,
|
||||
GoogleCreateAccountErrorType::BadInput => StatusCode::BAD_REQUEST,
|
||||
GoogleCreateAccountErrorType::EmailTaken => StatusCode::BAD_REQUEST,
|
||||
GoogleCreateAccountErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GoogleCreateAccountErrorType::UsernameReserved => StatusCode::BAD_REQUEST,
|
||||
GoogleCreateAccountErrorType::UsernameTaken => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a new account from "Sign in with Google" credentials.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Users",
|
||||
path = "/v1/accounts/google_create",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GoogleCreateAccountSuccessResponse),
|
||||
(status = 400, description = "Bad input", body = GoogleCreateAccountErrorResponse),
|
||||
(status = 401, description = "Not authorized", body = GoogleCreateAccountErrorResponse),
|
||||
(status = 500, description = "Server error", body = GoogleCreateAccountErrorResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GoogleCreateAccountRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn create_account_from_google_sign_in_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<GoogleCreateAccountRequest>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_cookie_manager: web::Data<HttpUserSessionManager>,
|
||||
) -> Result<HttpResponse, GoogleCreateAccountErrorResponse>
|
||||
{
|
||||
// TODO(bt,2024-09-20): Cache certs in shared server state
|
||||
let keys = download_cert_key_set()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error downloading google certs: {:?}", e);
|
||||
GoogleCreateAccountErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
let claims = decode_and_verify_token_claims(&keys, &request.google_credential, None)
|
||||
.map_err(|e| {
|
||||
warn!("error decoding google token claims: {:?}", e);
|
||||
GoogleCreateAccountErrorResponse::bad_request()
|
||||
})?;
|
||||
|
||||
info!("Google JWT credential claims: email {:?}, verified: {}",
|
||||
claims.email(),
|
||||
claims.email_verified());
|
||||
|
||||
let display_name = generate_random_username();
|
||||
let username = display_name.trim().to_lowercase();
|
||||
|
||||
info!("generated username: {}", username);
|
||||
|
||||
/*
|
||||
let mut error_fields = HashMap::new();
|
||||
|
||||
if let Err(reason) = validate_username(&request.username) {
|
||||
error_fields.insert("username".to_string(), reason);
|
||||
}
|
||||
|
||||
if let Err(reason) = validate_passwords(&request.password, &request.password_confirmation) {
|
||||
error_fields.insert("password".to_string(), reason);
|
||||
}
|
||||
|
||||
if contains_slurs(&request.username) {
|
||||
error_fields.insert("username".to_string(), "username contains slurs".to_string());
|
||||
}
|
||||
|
||||
if !request.email_address.contains("@") {
|
||||
error_fields.insert("email_address".to_string(), "invalid email address".to_string());
|
||||
}
|
||||
|
||||
if is_reserved_username(&request.username) {
|
||||
error_fields.insert("username".to_string(), "username is reserved".to_string());
|
||||
|
||||
return Err(GoogleCreateAccountErrorResponse {
|
||||
success: false,
|
||||
error_type: GoogleCreateAccountErrorType::UsernameReserved,
|
||||
error_fields
|
||||
});
|
||||
}
|
||||
|
||||
if !error_fields.is_empty() {
|
||||
return Err(GoogleCreateAccountErrorResponse {
|
||||
success: false,
|
||||
error_type: GoogleCreateAccountErrorType::BadInput,
|
||||
error_fields
|
||||
});
|
||||
}
|
||||
|
||||
let password_hash = match bcrypt_hash_password(request.password.clone()) {
|
||||
Ok(hash) => hash,
|
||||
Err(err) => {
|
||||
warn!("Bcrypt error: {:?}", err);
|
||||
return Err(GoogleCreateAccountErrorResponse::server_error());
|
||||
}
|
||||
};
|
||||
|
||||
let username = request.username.trim().to_lowercase();
|
||||
|
||||
let display_name = request.username.trim().to_string();
|
||||
|
||||
let email_address = request.email_address.trim().to_lowercase();
|
||||
|
||||
let email_gravatar_hash = email_to_gravatar(&email_address);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_origin = get_request_origin_uri(&http_request);
|
||||
|
||||
let mut maybe_source = None;
|
||||
|
||||
match maybe_origin {
|
||||
Ok(Some(uri)) => {
|
||||
if let Some(host) = uri.host() {
|
||||
if host.contains("storyteller") {
|
||||
maybe_source = Some("storyteller");
|
||||
} else if host.contains("fakeyou") {
|
||||
maybe_source = Some("fakeyou");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fail open for now.
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
warn!("Origin header error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let create_account_result = create_account(
|
||||
&mysql_pool,
|
||||
CreateAccountArgs {
|
||||
username: &username,
|
||||
display_name: &display_name,
|
||||
email_address: &email_address,
|
||||
email_gravatar_hash: &email_gravatar_hash,
|
||||
password_hash: &password_hash,
|
||||
ip_address: &ip_address,
|
||||
maybe_source,
|
||||
maybe_user_token: None, // NB: This parameter is for internal testing only
|
||||
}
|
||||
).await;
|
||||
|
||||
let new_user_data = match create_account_result {
|
||||
Ok(success) => success,
|
||||
Err(err) => {
|
||||
let mut error_fields = HashMap::new();
|
||||
let error_type;
|
||||
|
||||
match err {
|
||||
CreateAccountError::EmailIsTaken => {
|
||||
error_type = GoogleCreateAccountErrorType::EmailTaken;
|
||||
error_fields.insert("email_address".to_string(), "email is taken".to_string());
|
||||
}
|
||||
CreateAccountError::UsernameIsTaken => {
|
||||
error_type = GoogleCreateAccountErrorType::UsernameTaken;
|
||||
error_fields.insert("username".to_string(), "username is taken".to_string());
|
||||
}
|
||||
CreateAccountError::DatabaseError | CreateAccountError::OtherError => {
|
||||
error_type = GoogleCreateAccountErrorType::ServerError;
|
||||
}
|
||||
}
|
||||
|
||||
return Err(GoogleCreateAccountErrorResponse {
|
||||
success: false,
|
||||
error_type,
|
||||
error_fields
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
info!("new user id: {}", new_user_data.user_id);
|
||||
|
||||
let create_session_result = create_user_session(
|
||||
new_user_data.user_token.as_str(),
|
||||
&ip_address,
|
||||
&mysql_pool
|
||||
).await;
|
||||
|
||||
let session_token = match create_session_result {
|
||||
Ok(token) => token,
|
||||
Err(e) => {
|
||||
warn!("create account session creation error : {:?}", e);
|
||||
return Err(GoogleCreateAccountErrorResponse::server_error());
|
||||
}
|
||||
};
|
||||
|
||||
info!("new user session created");
|
||||
|
||||
firehose_publisher.publish_user_sign_up(new_user_data.user_token.as_str())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error publishing event: {:?}", e);
|
||||
GoogleCreateAccountErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
// NB: Enroll new users in studio for a while.
|
||||
enroll_in_studio(&new_user_data.user_token, &ip_address, &mysql_pool, None).await
|
||||
.map_err(|e| {
|
||||
warn!("error enrolling in studio: {:?}", e);
|
||||
}).ok();
|
||||
|
||||
let session_token = UserSessionToken::new_from_str(&session_token);
|
||||
|
||||
let session_cookie = match session_cookie_manager.create_cookie(&session_token, &new_user_data.user_token) {
|
||||
Ok(cookie) => cookie,
|
||||
Err(_) => return Err(GoogleCreateAccountErrorResponse::server_error()),
|
||||
};
|
||||
|
||||
let signed_session = match session_cookie_manager.encode_session_payload(&session_token, &new_user_data.user_token) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => return Err(GoogleCreateAccountErrorResponse::server_error()),
|
||||
};
|
||||
*/
|
||||
|
||||
let response = GoogleCreateAccountSuccessResponse {
|
||||
success: true,
|
||||
signed_session: "todo".to_string(),
|
||||
automatic_user_display_name: display_name,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| GoogleCreateAccountErrorResponse::server_error())?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
// .cookie(session_cookie) // TODO / FIXME
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod create_account_from_google_sign_in_handler;
|
||||
pub mod create_account_handler;
|
||||
pub mod edit_profile_handler;
|
||||
pub mod get_profile_handler;
|
||||
|
||||
@@ -6,6 +6,7 @@ use actix_web::error::Error;
|
||||
|
||||
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::create_account_from_google_sign_in_handler::create_account_from_google_sign_in_handler;
|
||||
use crate::http_server::endpoints::users::create_account_handler::create_account_handler;
|
||||
use crate::http_server::endpoints::users::edit_profile_handler::edit_profile_handler;
|
||||
use crate::http_server::endpoints::users::get_profile_handler::get_profile_handler;
|
||||
@@ -41,6 +42,11 @@ pub fn add_user_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::post().to(create_account_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/v1/accounts/google_create")
|
||||
.route(web::post().to(create_account_from_google_sign_in_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
|
||||
web::resource("/login")
|
||||
|
||||
Reference in new issue
Block a user