diff --git a/.idea/dictionaries/bt.xml b/.idea/dictionaries/bt.xml index 619c8a9aac..65423d1435 100644 --- a/.idea/dictionaries/bt.xml +++ b/.idea/dictionaries/bt.xml @@ -1,12 +1,9 @@ - arpabet backfill + cashapp cheermote fakeyou hifigan diff --git a/Cargo.lock b/Cargo.lock index 38ffd292c4..3aed6407b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,6 +1055,48 @@ dependencies = [ "tokio-util 0.6.9", ] +[[package]] +name = "component_subsystem_billing" +version = "0.0.1" +dependencies = [ + "actix-cors 0.6.0-beta.3", + "actix-http 3.0.0-beta.11", + "actix-web 4.0.0-beta.10", + "anyhow", + "base64 0.13.0", + "chrono", + "container_common", + "dotenv", + "log", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "toml", +] + +[[package]] +name = "component_subsystem_inference_workloads" +version = "0.0.1" +dependencies = [ + "actix-cors 0.6.0-beta.3", + "actix-http 3.0.0-beta.11", + "actix-web 4.0.0-beta.10", + "anyhow", + "base64 0.13.0", + "chrono", + "container_common", + "dotenv", + "log", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "toml", +] + [[package]] name = "config" version = "0.0.1" @@ -2065,6 +2107,82 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image-generation-service" +version = "0.0.1" +dependencies = [ + "actix", + "actix-cors 0.6.0-beta.3", + "actix-files", + "actix-http 3.0.0-beta.11", + "actix-multipart", + "actix-rt 2.4.0", + "actix-service 2.0.1", + "actix-utils 3.0.0", + "actix-web 4.0.0-beta.10", + "actix-web-actors", + "anyhow", + "base64 0.13.0", + "bcrypt", + "chrono", + "config", + "container_common", + "data-encoding", + "database_queries", + "derive_more", + "diesel", + "dotenv", + "easyenv", + "futures 0.1.31", + "futures 0.3.17", + "futures-util", + "google_drive_common", + "hex", + "hmac 0.10.1", + "hostname", + "http_server_common", + "hyper 0.14.14", + "infer", + "jobs_common", + "jwt", + "language-tags 0.3.2", + "lazy_static", + "lexical-sort", + "lfu", + "limitation", + "log", + "lru_time_cache", + "magic-crypt", + "md-5", + "memory_caching", + "native-tls", + "once_cell", + "pulldown-cmark", + "r2d2_redis", + "rand 0.8.4", + "redis_common", + "regex", + "reqwest", + "reusable_types", + "ring 0.17.0-alpha.10", + "rust-s3", + "serde", + "serde_derive", + "serde_json", + "sha2", + "sqlx", + "storage_buckets_common", + "subprocess", + "tempdir", + "time 0.2.26", + "tokio 1.13.0", + "toml", + "tts_common", + "twitch_common", + "user_input_common", + "uuid", +] + [[package]] name = "indexmap" version = "1.6.2" @@ -3923,6 +4041,7 @@ dependencies = [ "twitch_common", "twitch_oauth2", "user_input_common", + "users_component", "uuid", ] @@ -4812,10 +4931,42 @@ dependencies = [ "container_common", "log", "once_cell", + "pulldown-cmark", "regex", "unicode-segmentation", ] +[[package]] +name = "users_component" +version = "0.0.1" +dependencies = [ + "actix-web 4.0.0-beta.10", + "anyhow", + "base64 0.13.0", + "bcrypt", + "chrono", + "container_common", + "database_queries", + "dotenv", + "hmac 0.10.1", + "http_server_common", + "jwt", + "lazy_static", + "log", + "md-5", + "once_cell", + "regex", + "reusable_types", + "serde", + "serde_derive", + "serde_json", + "sha2", + "sqlx", + "time 0.2.26", + "toml", + "user_input_common", +] + [[package]] name = "utf-8" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index 64686d9906..b8328873b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [workspace] members = [ + # Complete subsystems that can be plugged into services + "crates/component_subsystem/billing", + "crates/component_subsystem/inference_workloads", + "crates/component_subsystem/users", + # Shared libraries "crates/lib/config", "crates/lib/container_common", @@ -16,8 +21,9 @@ members = [ "crates/lib/twitch_common", "crates/lib/user_input_common", - # Application + # Services: API servers, jobs, workers, etc. "crates/service/generic_download_job", + "crates/service/image_generation_service", "crates/service/storyteller_web", "crates/service/tts_download_job", "crates/service/tts_inference_job", diff --git a/_migrations_todo/2022-08-23-140455_credits/down.sql b/_migrations_todo/2022-08-23-140455_credits/down.sql new file mode 100644 index 0000000000..283831e5e7 --- /dev/null +++ b/_migrations_todo/2022-08-23-140455_credits/down.sql @@ -0,0 +1,9 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE paypal_payment_events; +DROP TABLE stripe_payment_events; +DROP TABLE credit_spend_events; +DROP TABLE credit_refill_events; +DROP TABLE credit_balances; diff --git a/_migrations_todo/2022-08-23-140455_credits/up.sql b/_migrations_todo/2022-08-23-140455_credits/up.sql new file mode 100644 index 0000000000..80a4389fc3 --- /dev/null +++ b/_migrations_todo/2022-08-23-140455_credits/up.sql @@ -0,0 +1,253 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Current credit balances +CREATE TABLE credit_balances ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- The type of credit + -- Each credit type is its own logical "bucket" that cannot be mixed with the other types. + -- eg. "image_generation" + -- We'll have a composite unique key on this, so there can only be one balance per user per type. + credit_type VARCHAR(32) NOT NULL, + + -- Foreign key to user + -- We'll have a composite unique key on this, so there can only be one balance per user per type. + user_token VARCHAR(32) NOT NULL, + + -- ========== BALANCE INFO ========== + + -- The credit balance + -- (for the user within this credit_type bucket) + -- Maximum value signed: 4,294,967,295 + balance INT UNSIGNED NOT NULL DEFAULT 0, + + -- ========== VECTOR CLOCK ========== + + -- Vector clock incremented on update + version INT UNSIGNED NOT NULL DEFAULT 0, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + last_refilled_at TIMESTAMP DEFAULT NULL, + last_spent_at TIMESTAMP DEFAULT NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (credit_type, user_token), + KEY index_credit_type (credit_type), + KEY fk_user_token (user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + + +-- Record of credit refill (eg. purchase) events, some of which may be free +-- At time of record creation, these are considered "already processed" and reflected in the "user_balances" table. +-- For all intents and purposes, this is just a historical ledger of changes to active balance. +CREATE TABLE credit_refill_events ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" + token VARCHAR(32) NOT NULL, + + -- The type of credit + -- Each credit type is its own logical "bucket" that cannot be mixed with the other types. + -- eg. "image_generation" + credit_type VARCHAR(32) NOT NULL, + + -- Foreign key to user (not unique!) + user_token VARCHAR(32) NOT NULL, + + -- ========== REFILL INFO ========== + + -- If we filled the balance for free, this will be set to true. + -- These may be rewards for users, subscriptions that refill buckets, etc. + is_free_event BOOL NOT NULL DEFAULT false, + + -- Added funds + -- Maximum value signed: 4,294,967,295 + amount_added INT UNSIGNED NOT NULL DEFAULT 0, + + -- What the balance should be after the funds are added + -- This is not the source of truth and is only used for debugging. + balance_after_added INT UNSIGNED NOT NULL DEFAULT 0, + + -- Where the balance increase came from. + -- eg. "stripe", "paypal", "coinbase", "free", etc. + funds_source VARCHAR(32) NOT NULL, + + -- ========== ABUSE TRACKING ========== + + -- For abuse tracking. + -- Wide enough for IPv4/6 + maybe_creator_ip_address VARCHAR(40) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_credit_type_and_user_token (credit_type, user_token), + KEY index_credit_type (credit_type), + KEY fk_user_token (user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + + +-- Record of credit use events (some of which may be zero-credit "free" uses) +-- At time of record creation, these are considered "already processed" and reflected in the "user_balances" table. +-- For all intents and purposes, this is just a historical ledger of changes to active balance. +CREATE TABLE credit_spend_events ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" + token VARCHAR(32) NOT NULL, + + -- The type of credit. + -- Each credit type is its own logical "bucket" that cannot be mixed with the other types. + -- eg. "image_generation" + credit_type VARCHAR(32) NOT NULL, + + -- Foreign key to user (not unique!) + user_token VARCHAR(32) NOT NULL, + + -- ========== SPEND INFO ========== + + -- If we processed the transaction for free, this will be set to true. + -- These may be rewards for users, subscriptions that refill, etc. + is_free_event BOOL NOT NULL DEFAULT false, + + -- Subtracted funds + -- Maximum value signed: 4,294,967,295 + amount_subtracted INT UNSIGNED NOT NULL DEFAULT 0, + + -- What the balance should be after the funds are subtracted + -- This is not the source of truth and is only used for debugging. + balance_after_subtracted INT UNSIGNED NOT NULL DEFAULT 0, + + -- Where the funds were spent. Typically 1:1 with "credit_type". + -- eg. "stable_diffusion", "voice_commission", etc. + spent_on VARCHAR(32) NOT NULL, + + -- If we can link it to whatever was purchased, do so. + -- If it was a job run, it's set here. + maybe_spent_on_job_token VARCHAR(32) DEFAULT NULL, + + -- If we can link it to whatever was purchased, do so. + -- We may not have the entity token if the workload was async, but we can update this record later. + maybe_spent_on_entity_token VARCHAR(32) DEFAULT NULL, + + -- ========== ABUSE TRACKING ========== + + -- For abuse tracking. + -- Wide enough for IPv4/6 + maybe_creator_ip_address VARCHAR(40) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_credit_type_and_user_token (credit_type, user_token), + KEY index_credit_type (credit_type), + KEY fk_user_token (user_token), + KEY fk_maybe_spent_on_job_token (maybe_spent_on_job_token), + KEY fk_maybe_spent_on_entity_token (maybe_spent_on_entity_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + + +-- Stripe payments +CREATE TABLE stripe_payment_events ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" + token VARCHAR(32) NOT NULL, + + -- Foreign key to user (not unique!) + user_token VARCHAR(32) NOT NULL, + + -- ========== PAYMENT INFO ========== + + -- Potentially an internal event token (eg. if this goes to a balance, not a subscription) + maybe_balance_refill_event_token VARCHAR(32) NOT NULL, + + -- The stripe payment ID + maybe_payment_id VARCHAR(64) NOT NULL, + + -- The stripe customer ID + maybe_customer_id VARCHAR(64) NOT NULL, + + -- ========== ABUSE TRACKING ========== + + -- For abuse tracking. + -- Wide enough for IPv4/6 + maybe_creator_ip_address VARCHAR(40) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_user_token (user_token), + KEY fk_maybe_balance_refill_event_token (maybe_balance_refill_event_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- Paypal payments +CREATE TABLE paypal_payment_events ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" + token VARCHAR(32) NOT NULL, + + -- Foreign key to user (not unique!) + user_token VARCHAR(32) NOT NULL, + + -- ========== PAYMENT INFO ========== + + -- Potentially an internal event token (eg. if this goes to a balance, not a subscription) + maybe_balance_refill_event_token VARCHAR(32) NOT NULL, + + -- The paypal payment ID + maybe_payment_id VARCHAR(64) NOT NULL, + + -- The paypal customer ID + maybe_customer_id VARCHAR(64) NOT NULL, + + -- ========== ABUSE TRACKING ========== + + -- For abuse tracking. + -- Wide enough for IPv4/6 + maybe_creator_ip_address VARCHAR(40) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_user_token (user_token), + KEY fk_maybe_balance_refill_event_token (maybe_balance_refill_event_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations_todo/2022-08-23-140535_generic_workloads/down.sql b/_migrations_todo/2022-08-23-140535_generic_workloads/down.sql new file mode 100644 index 0000000000..4125aef7b1 --- /dev/null +++ b/_migrations_todo/2022-08-23-140535_generic_workloads/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE generic_workloads; diff --git a/_migrations_todo/2022-08-23-140535_generic_workloads/up.sql b/_migrations_todo/2022-08-23-140535_generic_workloads/up.sql new file mode 100644 index 0000000000..b34fb831bc --- /dev/null +++ b/_migrations_todo/2022-08-23-140535_generic_workloads/up.sql @@ -0,0 +1,108 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE generic_workloads ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Idempotency token + -- This is so the frontend client doesn't submit duplicate jobs. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- ========== SUCCESS CASE ========== + + -- If the job completes successfully, this is the result token. + -- This is only populated on a successful result. + on_success_finished_entity_token VARCHAR(32) DEFAULT NULL, + + -- The type of the object will vary based on the type of the workload, + -- and we may include heuristics that auto-detect types in the future + on_success_finished_entity_type VARCHAR(32) DEFAULT NULL, + + -- ========== WORKLOAD DETAILS ========== + + -- A "well defined" type of workload + -- The first workload type will be "image_generation_request" + workload_type VARCHAR(32) NOT NULL, + + -- JSON serialized details about the workload + -- The schema varies and is controlled by the "workload_type" + -- TEXT – 64KB (65,535 characters); TEXT also requires 2 bytes overhead. + workload_details TEXT, + + -- ========== CREATOR DETAILS AND PREFERENCES ========== + + -- Foreign key to user + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- The creator can set a desired visibility for their data. + -- This does not always apply to every workload type. + -- Additionally, some workload types may require moderator approval prior + -- to being publicly listed, and this field has no bearing on that. + -- NB: DO NOT CHANGE THE ORDER OF THE ENUM VALUES DURING SCHEMA MIGRATIONS. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- ========== JOB SYSTEM DETAILS ========== + + -- Jobs begin as "pending", then transition to other states. + -- + -- * Pending = job is ready to go + -- * Started = job is running + -- * Complete_Success = job is done (success) + -- * Complete_Failure = job is done (failure) + -- * Attempt_Failed = job failed but may retry. + -- * Dead = job failed permanently. + -- + -- Pending -> Started -> Complete_Success + -- |-> Complete_Failure + -- \-> Attempt_Failed -> Started -> { Complete, Failed, Dead } + status ENUM( + 'pending', + 'started', + 'complete_success', + 'complete_failure', + 'attempt_failed', + 'dead') NOT NULL DEFAULT 'pending', + + -- We can track this against a "max_attempt_count" + attempt_count INT(3) NOT NULL DEFAULT 0, + + -- If there is a failure, tell the user why. + failure_reason VARCHAR(512) DEFAULT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Failed jobs will set a next attempt time. + -- Subsequent tries can increase the timeout. + -- Failures because of permissions require human intervention => [retry_at=null]. + -- Failures because of invalid files are dead => [status=dead]. + retry_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_on_workload_type (workload_type), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY index_status (status), + KEY index_creator_ip_address (creator_ip_address) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations_todo/2022-08-23-140631_generated_images/down.sql b/_migrations_todo/2022-08-23-140631_generated_images/down.sql new file mode 100644 index 0000000000..3c2304d721 --- /dev/null +++ b/_migrations_todo/2022-08-23-140631_generated_images/down.sql @@ -0,0 +1,7 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE generated_images; +DROP TABLE generated_image_batches; +DROP TABLE generated_image_request_logs; diff --git a/_migrations_todo/2022-08-23-140631_generated_images/up.sql b/_migrations_todo/2022-08-23-140631_generated_images/up.sql new file mode 100644 index 0000000000..74d883ec53 --- /dev/null +++ b/_migrations_todo/2022-08-23-140631_generated_images/up.sql @@ -0,0 +1,111 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Sometimes we'll block image generation requests. +-- This is a log of *everything* that gets sent to us so that we can track abuse +-- of the system and identify bad actors. +CREATE TABLE generated_image_request_logs( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- ========== REQUEST DETAILS ========== + + -- The text prompt + text_prompt VARCHAR(512) DEFAULT NULL, + + -- If the request consumed credits + request_was_paid BOOL NOT NULL DEFAULT false, + + -- If we determined the request was NSFW + request_was_nsfw BOOL NOT NULL DEFAULT false, + + -- If we determined we should block the request upfront + request_was_blocked BOOL NOT NULL DEFAULT false, + + -- ========== CREATOR DETAILS ========== + + -- Foreign key to user + -- If no user is logged in, this is null. + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- Each image generation request will produce multiple images. +-- This is the parent record for multiple images. +CREATE TABLE generated_image_batches ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- ========== CREATOR DETAILS ========== + + -- Foreign key to user + -- If no user is logged in, this is null. + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- (THIS MIGHT NOT BE USED) + -- NB: DO NOT SORT! + -- THIS MUST MATCH THE RESPECTIVE JOBS TABLE. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- This is the child record for single images within a generated batch +CREATE TABLE generated_images ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Reference to the batch this image belongs to. + batch_token VARCHAR(32) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/crates/component_subsystem/README.md b/crates/component_subsystem/README.md new file mode 100644 index 0000000000..75efd4eae1 --- /dev/null +++ b/crates/component_subsystem/README.md @@ -0,0 +1,5 @@ +http_endpoints_common +===================== +This directory contains Rust library packages that contain common and reusable HTTP endpoints. +These might be things that are incorporated into different services, but otherwise broadly reusable. +User account creation, login, billing, etc. diff --git a/crates/component_subsystem/billing/Cargo.toml b/crates/component_subsystem/billing/Cargo.toml new file mode 100644 index 0000000000..3aba31e8bc --- /dev/null +++ b/crates/component_subsystem/billing/Cargo.toml @@ -0,0 +1,38 @@ +[package] +publish = false +name = "component_subsystem_billing" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] + +[lib] +name = "component_subsystem_billing" +path = "src/lib.rs" + +[dependencies] + +# Internal +container_common = { path = "../../lib/container_common" } + +# External +anyhow = "1.0.40" +base64 = "0.13.0" +chrono = { version = "0.4.19", features = ["serde"] } +dotenv = "0.15.0" +log = "0.4.14" +once_cell = "1.9.0" +regex = "1.5.4" + +# Web +actix-cors = "0.6.0-beta.3" +actix-http = "3.0.0-beta.11" +actix-web = "4.0.0-beta.10" + +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/list_user_vocoders.rs b/crates/component_subsystem/billing/src/lib.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/users/list_user_vocoders.rs rename to crates/component_subsystem/billing/src/lib.rs diff --git a/crates/component_subsystem/inference_workloads/Cargo.toml b/crates/component_subsystem/inference_workloads/Cargo.toml new file mode 100644 index 0000000000..ee4ecb0b2d --- /dev/null +++ b/crates/component_subsystem/inference_workloads/Cargo.toml @@ -0,0 +1,38 @@ +[package] +publish = false +name = "component_subsystem_inference_workloads" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] + +[lib] +name = "component_subsystem_inference_workloads" +path = "src/lib.rs" + +[dependencies] + +# Internal +container_common = { path = "../../lib/container_common" } + +# External +anyhow = "1.0.40" +base64 = "0.13.0" +chrono = { version = "0.4.19", features = ["serde"] } +dotenv = "0.15.0" +log = "0.4.14" +once_cell = "1.9.0" +regex = "1.5.4" + +# Web +actix-cors = "0.6.0-beta.3" +actix-http = "3.0.0-beta.11" +actix-web = "4.0.0-beta.10" + +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" diff --git a/crates/component_subsystem/inference_workloads/src/lib.rs b/crates/component_subsystem/inference_workloads/src/lib.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/component_subsystem/users/Cargo.toml b/crates/component_subsystem/users/Cargo.toml new file mode 100644 index 0000000000..c4487fd195 --- /dev/null +++ b/crates/component_subsystem/users/Cargo.toml @@ -0,0 +1,55 @@ +[package] +publish = false +name = "users_component" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] + +[lib] +name = "users_component" +path = "src/lib.rs" + +[dependencies] + +# Internal +container_common = { path = "../../lib/container_common" } +database_queries = { path = "../../lib/database_queries" } +http_server_common = { path = "../../lib/http_server_common" } +reusable_types = { path = "../../lib/reusable_types" } +user_input_common = { path = "../../lib/user_input_common" } + +# External +anyhow = "1.0.40" +base64 = "0.13.0" +chrono = { version = "0.4.19", features = ["serde"] } +dotenv = "0.15.0" +lazy_static = "1.4.0" # Regex caching (TODO: Use OnceCell instead) +log = "0.4.14" +once_cell = "1.9.0" +regex = "1.5.4" +time = "0.2.26" + +# Web +actix-web = "4.0.0-beta.10" +jwt = "0.13.0" + +# Crypto +bcrypt = "0.8" # password hashes +hmac = "0.10.0" # cookies +md-5 = "0.9.1" # gravatar +sha2 = "0.9.5" # file hashes, inference text hashes, etc. + +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" + +# MySql Database +# "offline" supports typesafe builds without a live database +# "rustls" doesn't appear to work with IP addresses, per: +# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828 +sqlx = { version = "0.5", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] } diff --git a/crates/component_subsystem/users/src/default_routes.rs b/crates/component_subsystem/users/src/default_routes.rs new file mode 100644 index 0000000000..ed5b2c830f --- /dev/null +++ b/crates/component_subsystem/users/src/default_routes.rs @@ -0,0 +1,46 @@ +//! These routes are recommended, but do not have to be used by consumers of the user system. +use actix_web::body::MessageBody; +use actix_web::dev::{ServiceRequest, ServiceResponse, ServiceFactory}; +use actix_web::error::Error; +use actix_web::{App, web, HttpResponse}; +use crate::endpoints::create_account_handler::create_account_handler; +use crate::endpoints::login_handler::login_handler; +use crate::endpoints::logout_handler::logout_handler; +use crate::endpoints::session_info_handler::session_info_handler; + +// NB: This does not include the user edit endpoints since FakeYou's API mounts them alongside other things. +// A 'v2' API would mount under a v2 prefix and the entity type. + +pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app + .service( + web::resource("/create_account") + .route(web::post().to(create_account_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service( + web::resource("/login") + .route(web::post().to(login_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service( + web::resource("/logout") + .route(web::post().to(logout_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service( + web::resource("/session") + .route(web::get().to(session_info_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) +} diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/create_account.rs b/crates/component_subsystem/users/src/endpoints/create_account_handler.rs similarity index 53% rename from crates/service/storyteller_web/src/http_server/endpoints/users/create_account.rs rename to crates/component_subsystem/users/src/endpoints/create_account_handler.rs index d61fb24fd9..a14ad654cc 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/create_account.rs +++ b/crates/component_subsystem/users/src/endpoints/create_account_handler.rs @@ -1,34 +1,29 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; -use crate::http_server::endpoints::users::create_account::CreateAccountErrorType::{BadInput, ServerError, UsernameTaken, EmailTaken}; -use crate::http_server::web_utils::ip_address::get_request_ip; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error; -use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; -use crate::validations::username_reservations::is_reserved_username; -use database_queries::tokens::Tokens; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::utils::email_to_gravatar::email_to_gravatar; +use crate::utils::session_cookie_manager::SessionCookieManager; +use crate::validations::is_reserved_username::is_reserved_username; +use crate::validations::validate_passwords::validate_passwords; +use crate::validations::validate_username::validate_username; +use database_queries::mediators::firehose_publisher::FirehosePublisher; +use database_queries::queries::users::create_account::{create_account, CreateAccountArgs, CreateAccountError}; use database_queries::queries::users::user_sessions::create_session::create_session_for_user; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use database_queries::tokens::Tokens; +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 sqlx::MySqlPool; use std::collections::HashMap; use std::fmt::Formatter; use std::fmt; -use std::sync::Arc; use user_input_common::check_for_slurs::contains_slurs; -const NEW_USER_ROLE: &'static str = "user"; - #[derive(Deserialize)] pub struct CreateAccountRequest { pub username: String, @@ -94,7 +89,10 @@ impl ResponseError for CreateAccountErrorResponse { pub async fn create_account_handler( http_request: HttpRequest, request: web::Json, - server_state: web::Data>) -> Result + mysql_pool: web::Data, + session_cookie_manager: web::Data, + firehose_publisher: web::Data, +) -> Result { let mut error_fields = HashMap::new(); @@ -147,106 +145,62 @@ pub async fn create_account_handler( }; 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 profile_markdown = ""; - let profile_rendered_html = ""; - let ip_address = get_request_ip(&http_request); - let query_result = sqlx::query!( - r#" -INSERT INTO users ( - token, - username, - display_name, - email_address, - email_gravatar_hash, - profile_markdown, - profile_rendered_html, - user_role_slug, - password_hash, - ip_address_creation, - ip_address_last_login, - ip_address_last_update -) -VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) - "#, - user_token.to_string(), - username, - display_name, - email_address, - email_gravatar_hash, - profile_markdown, - profile_rendered_html, - NEW_USER_ROLE, - password_hash, - ip_address.to_string(), - ip_address.to_string(), - ip_address.to_string(), - ) - .execute(&server_state.mysql_pool) - .await; + 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, + } + ).await; - let record_id = match query_result { - Ok(res) => { - res.last_insert_id() - }, + let new_user_data = match create_account_result { + Ok(success) => success, Err(err) => { - warn!("New user creation DB error: {:?}", err); + let mut error_fields = HashMap::new(); + let error_type; - // NB: SQLSTATE[23000]: Integrity constraint violation - // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { - Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); - let mut error_fields = HashMap::new(); - let mut maybe_error_type = None; - - match maybe_code.as_deref() { - Some("23000") => { - if err.message().contains("username") { - maybe_error_type = Some(CreateAccountErrorType::UsernameTaken); - error_fields.insert("username".to_string(), "username is taken".to_string()); - } else if err.message().contains("email_address") { - maybe_error_type = Some(CreateAccountErrorType::EmailTaken); - error_fields.insert("email_address".to_string(), "email is taken".to_string()); - } - } - _ => {}, - } - - if let Some(error_type) = maybe_error_type { - return Err(CreateAccountErrorResponse { - success: false, - error_type, - error_fields - }) - } - }, - _ => {}, + CreateAccountError::EmailIsTaken => { + error_type = CreateAccountErrorType::EmailTaken; + error_fields.insert("email_address".to_string(), "email is taken".to_string()); + } + CreateAccountError::UsernameIsTaken => { + error_type = CreateAccountErrorType::UsernameTaken; + error_fields.insert("username".to_string(), "username is taken".to_string()); + } + CreateAccountError::DatabaseError | CreateAccountError::OtherError => { + error_type = CreateAccountErrorType::ServerError; + } } - return Err(CreateAccountErrorResponse::server_error()); + + return Err(CreateAccountErrorResponse { + success: false, + error_type, + error_fields + }); } }; - info!("new user id: {}", record_id); + info!("new user id: {}", new_user_data.user_id); - //// TODO: Remove me in the future. Too lazy to check the date, plus this will - //// vary with adoption rate. - //server_state.badge_granter.grant_early_user_badge(&user_token) - // .await - // .map_err(|e| { - // warn!("error creating badge: {:?}", e); - // CreateAccountErrorResponse::server_error() - // })?; - - let create_session_result = - create_session_for_user(&user_token, &ip_address, &server_state.mysql_pool).await; + let create_session_result = create_session_for_user( + &new_user_data.user_token, + &ip_address, + &mysql_pool + ).await; let session_token = match create_session_result { Ok(token) => token, @@ -258,14 +212,14 @@ VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) info!("new user session created"); - server_state.firehose_publisher.publish_user_sign_up(&user_token) + firehose_publisher.publish_user_sign_up(&user_token) .await .map_err(|e| { warn!("error publishing event: {:?}", e); CreateAccountErrorResponse::server_error() })?; - let session_cookie = match server_state.cookie_manager.create_cookie(&session_token) { + let session_cookie = match session_cookie_manager.create_cookie(&session_token) { Ok(cookie) => cookie, Err(_) => return Err(CreateAccountErrorResponse::server_error()), }; @@ -275,7 +229,7 @@ VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) }; let body = serde_json::to_string(&response) - .map_err(|e| CreateAccountErrorResponse::server_error())?; + .map_err(|_e| CreateAccountErrorResponse::server_error())?; Ok(HttpResponse::Ok() .cookie(session_cookie) diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/edit_profile.rs b/crates/component_subsystem/users/src/endpoints/edit_profile_handler.rs similarity index 60% rename from crates/service/storyteller_web/src/http_server/endpoints/users/edit_profile.rs rename to crates/component_subsystem/users/src/endpoints/edit_profile_handler.rs index 83682fdc82..75a429dc26 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/edit_profile.rs +++ b/crates/component_subsystem/users/src/endpoints/edit_profile_handler.rs @@ -1,35 +1,31 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::Path; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; -use crate::http_server::web_utils::ip_address::get_request_ip; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; -use crate::validations::cashapp_username::{validate_cashapp_username, normalize_cashapp_username_for_storage}; -use crate::validations::discord_username::validate_discord_username; -use crate::validations::github_username::validate_github_username; -use crate::validations::passwords::validate_passwords; -use crate::validations::twitch_username::validate_twitch_username; -use crate::validations::twitter_username::{validate_twitter_username, normalize_twitter_username_for_storage}; -use crate::validations::username::validate_username; -use crate::validations::username_reservations::is_reserved_username; -use crate::validations::website_url::validate_website_url; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::utils::session_checker::SessionChecker; +use crate::validations::validate_profile_cashapp_username::{normalize_cashapp_username_for_storage, validate_profile_cashapp_username}; +use crate::validations::validate_profile_discord_username::validate_profile_discord_username; +use crate::validations::validate_profile_github_username::validate_profile_github_username; +use crate::validations::validate_profile_twitch_username::validate_profile_twitch_username; +use crate::validations::validate_profile_twitter_username::{normalize_twitter_username_for_storage, validate_profile_twitter_username}; +use crate::validations::validate_profile_website_url::validate_profile_website_url; use database_queries::column_types::record_visibility::RecordVisibility; -use database_queries::queries::users::get_user_profile_by_username::get_user_profile_by_username; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use database_queries::queries::users::user_profiles::edit_user_profile_as_account_holder::edit_user_profile_as_account_holder; +use database_queries::queries::users::user_profiles::edit_user_profile_as_mod::edit_user_profile_as_mod; +use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username; +use database_queries::queries::users::user_profiles::{edit_user_profile_as_account_holder, edit_user_profile_as_mod}; +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::warn; +use sqlx::MySqlPool; use std::fmt; -use std::sync::Arc; use user_input_common::check_for_slurs::contains_slurs; +use user_input_common::markdown_to_html::markdown_to_html; /// For the URL PathInfo #[derive(Deserialize)] @@ -60,7 +56,7 @@ pub struct EditProfileSuccessResponse { pub success: bool, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum EditProfileError { BadInput(String), NotAuthorized, @@ -79,14 +75,7 @@ impl ResponseError for EditProfileError { } fn error_response(&self) -> HttpResponse { - let error_reason = match self { - EditProfileError::BadInput(reason) => reason.to_string(), - EditProfileError::NotAuthorized=> "unauthorized".to_string(), - EditProfileError::UserNotFound => "user not found".to_string(), - EditProfileError::ServerError => "server error".to_string(), - }; - - to_simple_json_error(&error_reason, self.status_code()) + serialize_as_json_error(self) } } @@ -101,11 +90,12 @@ pub async fn edit_profile_handler( http_request: HttpRequest, path: Path, request: web::Json, - server_state: web::Data>) -> Result + mysql_pool: web::Data, + session_checker: web::Data, +) -> Result { - let maybe_user_session = server_state - .session_checker - .maybe_get_user_session(&http_request, &server_state.mysql_pool) + let maybe_user_session = session_checker + .maybe_get_user_session(&http_request, &mysql_pool) .await .map_err(|e| { warn!("Session checker error: {:?}", e); @@ -126,7 +116,7 @@ pub async fn edit_profile_handler( } let user_lookup_result = - get_user_profile_by_username(&path.username, &server_state.mysql_pool) + get_user_profile_by_username(&path.username, &mysql_pool) .await; let user_record = match user_lookup_result { @@ -168,7 +158,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { twitter_username = None; } else { - if let Err(reason) = validate_twitter_username(trimmed) { + if let Err(reason) = validate_profile_twitter_username(trimmed) { return Err(EditProfileError::BadInput(reason)); } let normalized = normalize_twitter_username_for_storage(trimmed); @@ -181,7 +171,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { twitch_username = None; } else { - if let Err(reason) = validate_twitch_username(trimmed) { + if let Err(reason) = validate_profile_twitch_username(trimmed) { return Err(EditProfileError::BadInput(reason)); } twitch_username = Some(trimmed); @@ -193,7 +183,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { discord_username = None; } else { - if let Err(reason) = validate_discord_username(trimmed) { + if let Err(reason) = validate_profile_discord_username(trimmed) { return Err(EditProfileError::BadInput(reason)); } discord_username = Some(trimmed); @@ -205,7 +195,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { github_username = None; } else { - if let Err(reason) = validate_github_username(trimmed) { + if let Err(reason) = validate_profile_github_username(trimmed) { return Err(EditProfileError::BadInput(reason)); } github_username = Some(trimmed); @@ -217,7 +207,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { cashapp_username = None; } else { - if let Err(reason) = validate_cashapp_username(trimmed) { + if let Err(reason) = validate_profile_cashapp_username(trimmed) { return Err(EditProfileError::BadInput(reason)); } let normalized = normalize_cashapp_username_for_storage(trimmed); @@ -230,7 +220,7 @@ pub async fn edit_profile_handler( if trimmed.is_empty() { website_url = None; } else { - if let Err(reason) = validate_website_url(trimmed) { + if let Err(reason) = validate_profile_website_url(trimmed) { return Err(EditProfileError::BadInput(reason)); } website_url = Some(trimmed); @@ -258,74 +248,40 @@ pub async fn edit_profile_handler( .unwrap_or(RecordVisibility::Hidden); let query_result = if editor_is_original_user { - // We need to store the IP address details. - sqlx::query!( - r#" -UPDATE users -SET - profile_markdown = ?, - profile_rendered_html = ?, - preferred_tts_result_visibility = ?, - preferred_w2l_result_visibility = ?, - discord_username = ?, - twitter_username = ?, - twitch_username = ?, - github_username = ?, - cashapp_username = ?, - website_url = ?, - ip_address_last_update = ?, - version = version + 1 - -WHERE users.token = ? -LIMIT 1 - "#, - profile_markdown, - profile_html, - preferred_tts_result_visibility.to_str(), - preferred_w2l_result_visibility.to_str(), - discord_username, - twitter_username, - twitch_username, - github_username, - cashapp_username, - website_url, - ip_address.clone(), - user_record.user_token.clone(), - ) - .execute(&server_state.mysql_pool) - .await + edit_user_profile_as_account_holder( + &mysql_pool, + edit_user_profile_as_account_holder::Args { + user_token: &user_record.user_token, + profile_markdown: profile_markdown.as_deref(), + profile_html: profile_html.as_deref(), + discord_username: discord_username.as_deref(), + twitter_username: twitter_username.as_deref(), + cashapp_username: cashapp_username.as_deref(), + github_username: github_username.as_deref(), + twitch_username: twitch_username.as_deref(), + website_url: website_url.as_deref(), + preferred_tts_result_visibility: preferred_tts_result_visibility.to_str(), + preferred_w2l_result_visibility: preferred_w2l_result_visibility.to_str(), + ip_address: &ip_address, + } + ).await } else { - // We need to store the moderator details. + // TODO(2022-09-01): We need to store the moderator details or have an audit log. // Also, mods shouldn't change user preferences. - sqlx::query!( - r#" -UPDATE users -SET - profile_markdown = ?, - profile_rendered_html = ?, - discord_username = ?, - twitter_username = ?, - twitch_username = ?, - github_username = ?, - cashapp_username = ?, - website_url = ?, - version = version + 1 - -WHERE users.token = ? -LIMIT 1 - "#, - profile_markdown, - profile_html, - discord_username, - twitter_username, - twitch_username, - github_username, - cashapp_username, - website_url, - user_record.user_token.clone(), - ) - .execute(&server_state.mysql_pool) - .await + edit_user_profile_as_mod( + &mysql_pool, + edit_user_profile_as_mod::Args { + user_token: &user_record.user_token, + profile_markdown: profile_markdown.as_deref(), + profile_html: profile_html.as_deref(), + discord_username: discord_username.as_deref(), + twitter_username: twitter_username.as_deref(), + cashapp_username: cashapp_username.as_deref(), + github_username: github_username.as_deref(), + twitch_username: twitch_username.as_deref(), + website_url: website_url.as_deref(), + } + ).await }; match query_result { @@ -341,7 +297,7 @@ LIMIT 1 }; let body = serde_json::to_string(&response) - .map_err(|e| EditProfileError::BadInput("".to_string()))?; + .map_err(|_e| EditProfileError::BadInput("".to_string()))?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/get_profile.rs b/crates/component_subsystem/users/src/endpoints/get_profile_handler.rs similarity index 82% rename from crates/service/storyteller_web/src/http_server/endpoints/users/get_profile.rs rename to crates/component_subsystem/users/src/endpoints/get_profile_handler.rs index 62ae371ac1..8b03444447 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/get_profile.rs +++ b/crates/component_subsystem/users/src/endpoints/get_profile_handler.rs @@ -1,30 +1,24 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::Path; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest, HttpMessage}; -use anyhow::anyhow; +use actix_web::{web, HttpResponse, HttpRequest}; use chrono::{DateTime, Utc}; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::server_state::ServerState; -use database_queries::queries::users::get_user_profile_by_username::{get_user_profile_by_username, UserProfileResult, get_user_profile_by_username_from_connection}; +use crate::utils::session_checker::SessionChecker; use database_queries::queries::users::user_badges::list_user_badges::UserBadgeForList; use database_queries::queries::users::user_badges::list_user_badges::list_user_badges; +use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username_from_connection; use http_server_common::request::get_request_header_optional::get_request_header_optional; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use http_server_common::util::timer::MultiBenchmarkingTimer; -use log::{info, warn, log, error}; -use md5::{Md5, Digest}; -use regex::Regex; +use log::warn; use reusable_types::entity_visibility::EntityVisibility; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; -use std::sync::Arc; // TODO: This is duplicated in query_user_profile // TODO: This handler has embedded queries. @@ -66,7 +60,7 @@ pub struct ProfileSuccessResponse { pub user: Option, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub enum ProfileError { ServerError, NotFound, @@ -87,12 +81,7 @@ impl ResponseError for ProfileError { } fn error_response(&self) -> HttpResponse { - let error_reason = match self { - ProfileError::ServerError => "server error".to_string(), - ProfileError::NotFound => "not found error".to_string(), - }; - - to_simple_json_error(&error_reason, self.status_code()) + serialize_as_json_error(self) } } @@ -106,21 +95,22 @@ impl fmt::Display for ProfileError { pub async fn get_profile_handler( http_request: HttpRequest, path: Path, - server_state: web::Data>) -> Result + mysql_pool: web::Data, + session_checker: web::Data, +) -> Result { let mut benchmark = MultiBenchmarkingTimer::new_started(); - let mut pool_connection = server_state.mysql_pool.acquire() + let pool_connection = mysql_pool.acquire() .await .map_err(|e| { warn!("Could not acquire DB pool: {:?}", e); ProfileError::ServerError })?; - let (mut pool_connection, maybe_user_session_fut) = + let (pool_connection, maybe_user_session_fut) = benchmark.time_async_section_moving_args("session checker", pool_connection, |mut pc| async { - let ret = server_state - .session_checker + let ret = session_checker .maybe_get_user_session_from_connection(&http_request, &mut pc) .await .map_err(|e| { @@ -139,13 +129,13 @@ pub async fn get_profile_handler( is_mod = user_session.can_ban_users; } - let (mut pool_connection, maybe_user_profile) = + let (pool_connection, maybe_user_profile) = benchmark.time_async_section_moving_args("profile query", pool_connection, |mut pc| async { let ret = get_user_profile_by_username_from_connection(&path.username, &mut pc).await; (pc, ret) }).await; - let mut user_profile = match maybe_user_profile { + let user_profile = match maybe_user_profile { Ok(Some(user_profile)) => user_profile, Ok(None) => { warn!("Invalid user"); @@ -221,7 +211,7 @@ pub async fn get_profile_handler( }; let body = serde_json::to_string(&response) - .map_err(|e| ProfileError::ServerError)?; + .map_err(|_e| ProfileError::ServerError)?; let mut http_response = HttpResponse::Ok(); diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/login.rs b/crates/component_subsystem/users/src/endpoints/login_handler.rs similarity index 63% rename from crates/service/storyteller_web/src/http_server/endpoints/users/login.rs rename to crates/component_subsystem/users/src/endpoints/login_handler.rs index c1b9745be0..b1c64338d3 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/login.rs +++ b/crates/component_subsystem/users/src/endpoints/login_handler.rs @@ -1,26 +1,22 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; -use crate::AnyhowResult; -use crate::http_server::web_utils::ip_address::get_request_ip; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error; -use crate::server_state::ServerState; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::utils::session_cookie_manager::SessionCookieManager; use database_queries::helpers::boolean_converters::i8_to_bool; use database_queries::queries::users::user_sessions::create_session::create_session_for_user; -use log::{info, warn, log}; -use regex::Regex; +use database_queries::queries::users::user_sessions::lookup_user_for_login_by_email::lookup_user_for_login_by_email; +use database_queries::queries::users::user_sessions::lookup_user_for_login_by_username::lookup_user_for_login_by_username; +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 sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt::Formatter; use std::fmt; -use std::sync::Arc; #[derive(Deserialize)] pub struct LoginRequest { @@ -86,14 +82,16 @@ impl ResponseError for LoginErrorResponse { pub async fn login_handler( http_request: HttpRequest, request: web::Json, - server_state: web::Data>) -> Result + session_cookie_manager: web::Data, + mysql_pool: web::Data, +) -> Result { let check_username_or_email = request.username_or_email.to_lowercase(); let maybe_user = if check_username_or_email.contains("@") { - lookup_by_email(&check_username_or_email, &server_state.mysql_pool).await + lookup_user_for_login_by_email(&check_username_or_email, &mysql_pool).await } else { - lookup_by_username(&check_username_or_email, &server_state.mysql_pool).await + lookup_user_for_login_by_username(&check_username_or_email, &mysql_pool).await }; let user = match maybe_user { @@ -139,7 +137,7 @@ pub async fn login_handler( let ip_address = get_request_ip(&http_request); let create_session_result = - create_session_for_user(&user.token, &ip_address, &server_state.mysql_pool).await; + create_session_for_user(&user.token, &ip_address, &mysql_pool).await; let session_token = match create_session_result { Ok(token) => token, @@ -151,7 +149,7 @@ pub async fn login_handler( info!("login session created"); - let session_cookie = match server_state.cookie_manager.create_cookie(&session_token) { + let session_cookie = match session_cookie_manager.create_cookie(&session_token) { Ok(cookie) => cookie, Err(_) => return Err(LoginErrorResponse::server_error()), }; @@ -161,56 +159,10 @@ pub async fn login_handler( }; let body = serde_json::to_string(&response) - .map_err(|e| LoginErrorResponse::server_error())?; + .map_err(|_e| LoginErrorResponse::server_error())?; Ok(HttpResponse::Ok() .cookie(session_cookie) .content_type("application/json") .body(body)) } - -#[derive(Debug)] -pub struct UserRecordForLogin { - token: String, - username: String, - email_address: String, - password_hash: Vec, - is_banned: i8, -} - -async fn lookup_by_username(username: &str, pool: &MySqlPool) -> AnyhowResult -{ - // NB: Lookup failure is Err(RowNotFound). - let record = sqlx::query_as!( - UserRecordForLogin, - r#" -SELECT token, username, email_address, password_hash, is_banned -FROM users -WHERE username = ? -LIMIT 1 - "#, - username.to_string(), - ) - .fetch_one(pool) - .await?; - - Ok(record) -} - -async fn lookup_by_email(email: &str, pool: &MySqlPool) -> AnyhowResult { - // NB: Lookup failure is Err(RowNotFound). - let record = sqlx::query_as!( - UserRecordForLogin, - r#" -SELECT token, username, email_address, password_hash, is_banned -FROM users -WHERE email_address = ? -LIMIT 1 - "#, - email.to_string(), - ) - .fetch_one(pool) - .await?; - - Ok(record) -} diff --git a/crates/component_subsystem/users/src/endpoints/logout_handler.rs b/crates/component_subsystem/users/src/endpoints/logout_handler.rs new file mode 100644 index 0000000000..9510e544e2 --- /dev/null +++ b/crates/component_subsystem/users/src/endpoints/logout_handler.rs @@ -0,0 +1,84 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::utils::session_cookie_manager::SessionCookieManager; +use database_queries::queries::users::user_sessions::delete_session::delete_session; +use http_server_common::response::response_error_helpers::to_simple_json_error; +use log::warn; +use sqlx::MySqlPool; +use std::fmt; + +#[derive(Serialize)] +pub struct LogoutSuccessResponse { + pub success: bool, +} + +#[derive(Debug)] +pub enum LogoutError { + ServerError, +} + +impl ResponseError for LogoutError { + fn status_code(&self) -> StatusCode { + match *self { + LogoutError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + LogoutError::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 LogoutError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn logout_handler( + http_request: HttpRequest, + session_cookie_manager: web::Data, + mysql_pool: web::Data, +) -> Result +{ + let delete_cookie = match http_request.cookie("session") { + Some(cookie) => { + match session_cookie_manager.decode_session_token(&cookie) { + Err(e) => { + warn!("Session cookie decode error: {:?}", e); + }, + Ok(session_token) => { + let _r = delete_session(&session_token, &mysql_pool).await; + } + } + + cookie // delete this cookie + }, + None => { + session_cookie_manager.delete_cookie() + } + }; + + let response = LogoutSuccessResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| LogoutError::ServerError)?; + + Ok(HttpResponse::Ok() + .del_cookie(&delete_cookie) + .content_type("application/json") + .body(body)) +} diff --git a/crates/component_subsystem/users/src/endpoints/mod.rs b/crates/component_subsystem/users/src/endpoints/mod.rs new file mode 100644 index 0000000000..ddbd3ba54a --- /dev/null +++ b/crates/component_subsystem/users/src/endpoints/mod.rs @@ -0,0 +1,6 @@ +pub mod create_account_handler; +pub mod edit_profile_handler; +pub mod get_profile_handler; +pub mod login_handler; +pub mod logout_handler; +pub mod session_info_handler; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/session_info.rs b/crates/component_subsystem/users/src/endpoints/session_info_handler.rs similarity index 93% rename from crates/service/storyteller_web/src/http_server/endpoints/users/session_info.rs rename to crates/component_subsystem/users/src/endpoints/session_info_handler.rs index 9fb1e4792d..b09065dc12 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/session_info.rs +++ b/crates/component_subsystem/users/src/endpoints/session_info_handler.rs @@ -6,11 +6,11 @@ use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::{web, HttpResponse, HttpRequest}; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::server_state::ServerState; +use crate::utils::session_checker::SessionChecker; +use http_server_common::response::response_error_helpers::to_simple_json_error; use log::warn; +use sqlx::MySqlPool; use std::fmt; -use std::sync::Arc; #[derive(Serialize, Copy, Clone)] #[serde(rename_all = "snake_case")] @@ -104,17 +104,18 @@ impl fmt::Display for SessionInfoError { pub async fn session_info_handler( http_request: HttpRequest, - server_state: web::Data>) -> Result + mysql_pool: web::Data, + session_checker: web::Data, +) -> Result { - let mut mysql_connection = server_state.mysql_pool.acquire() + let mut mysql_connection = mysql_pool.acquire() .await .map_err(|e| { warn!("Could not acquire DB pool: {:?}", e); SessionInfoError::ServerError })?; - let maybe_user_session = server_state - .session_checker + let maybe_user_session = session_checker .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) .await .map_err(|e| { diff --git a/crates/component_subsystem/users/src/lib.rs b/crates/component_subsystem/users/src/lib.rs new file mode 100644 index 0000000000..8d23f8f9f6 --- /dev/null +++ b/crates/component_subsystem/users/src/lib.rs @@ -0,0 +1,10 @@ +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate serde_derive; + +pub const RESERVED_USERNAMES : &'static str = include_str!("../../../../db/reserved_usernames.txt"); +pub const RESERVED_SUBSTRINGS : &'static str = include_str!("../../../../db/reserved_usernames_including.txt"); + +pub mod default_routes; +pub mod endpoints; +pub mod utils; +pub mod validations; \ No newline at end of file diff --git a/crates/component_subsystem/users/src/utils/email_to_gravatar.rs b/crates/component_subsystem/users/src/utils/email_to_gravatar.rs new file mode 100644 index 0000000000..bec614a1be --- /dev/null +++ b/crates/component_subsystem/users/src/utils/email_to_gravatar.rs @@ -0,0 +1,34 @@ +use md5::{Md5, Digest}; + +pub fn email_to_gravatar(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::utils::email_to_gravatar::email_to_gravatar; + + #[test] + fn test_gravatar() { + // Email 1 + assert_eq!(email_to_gravatar("example@example.com"), "".to_string()); + assert_eq!(email_to_gravatar("EXAMPLE@EXAMPLE.COM"), "".to_string()); + assert_eq!(email_to_gravatar(" example@example.com \n "), "".to_string()); + + // Email 2 + assert_eq!(email_to_gravatar("echelon@gmail.com"), "".to_string()); + assert_eq!(email_to_gravatar("ECHELON@GMAIL.COM"), "".to_string()); + assert_eq!(email_to_gravatar(" ECHELON@GMAIL.COM "), "".to_string()); + + // Misc + assert_eq!(email_to_gravatar(""), "".to_string()); + assert_eq!(email_to_gravatar(" "), "".to_string()); + } +} \ No newline at end of file diff --git a/crates/component_subsystem/users/src/utils/mod.rs b/crates/component_subsystem/users/src/utils/mod.rs new file mode 100644 index 0000000000..6e549cc127 --- /dev/null +++ b/crates/component_subsystem/users/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod email_to_gravatar; +pub mod session_checker; +pub mod session_cookie_manager; \ No newline at end of file diff --git a/crates/component_subsystem/users/src/utils/session_checker.rs b/crates/component_subsystem/users/src/utils/session_checker.rs new file mode 100644 index 0000000000..d7d0f1c47e --- /dev/null +++ b/crates/component_subsystem/users/src/utils/session_checker.rs @@ -0,0 +1,78 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_web::HttpRequest; +use container_common::anyhow_result::AnyhowResult; +use crate::utils::session_cookie_manager::SessionCookieManager; +use database_queries::queries::users::user_sessions::get_session_by_token::{get_session_by_token, SessionUserRecord}; +use database_queries::queries::users::user_sessions::get_session_by_token_light::{get_session_by_token_light, SessionRecord}; +use sqlx::pool::PoolConnection; +use sqlx::{MySqlPool, MySql}; + +#[derive(Clone)] +pub struct SessionChecker { + cookie_manager: SessionCookieManager, +} + +impl SessionChecker { + + pub fn new(cookie_manager: &SessionCookieManager) -> Self { + Self { + cookie_manager: cookie_manager.clone(), + } + } + + #[deprecated = "Use the PoolConnection method instead of the MySqlPool one."] + pub async fn maybe_get_session( + &self, + request: &HttpRequest, + pool: &MySqlPool + ) -> AnyhowResult> + { + let mut connection = pool.acquire().await?; + self.maybe_get_session_from_connection(request, &mut connection).await + } + + pub async fn maybe_get_session_from_connection( + &self, + request: &HttpRequest, + mysql_connection: &mut PoolConnection, + ) -> AnyhowResult> + { + let session_token = match self.cookie_manager.decode_session_token_from_request(request)? { + None => return Ok(None), + Some(session_token) => session_token, + }; + + get_session_by_token_light(mysql_connection, &session_token).await + } + + + #[deprecated = "Use the PoolConnection method instead of the MySqlPool one."] + pub async fn maybe_get_user_session( + &self, + request: &HttpRequest, + pool: &MySqlPool, + ) -> AnyhowResult> + { + let mut connection = pool.acquire().await?; + self.maybe_get_user_session_from_connection(request, &mut connection).await + } + + pub async fn maybe_get_user_session_from_connection( + &self, + request: &HttpRequest, + mysql_connection: &mut PoolConnection, + ) -> AnyhowResult> + { + let session_token = match self.cookie_manager.decode_session_token_from_request(request)? { + None => return Ok(None), + Some(session_token) => session_token, + }; + + get_session_by_token(mysql_connection, &session_token).await + } +} + diff --git a/crates/service/storyteller_web/src/http_server/web_utils/cookie_manager.rs b/crates/component_subsystem/users/src/utils/session_cookie_manager.rs similarity index 84% rename from crates/service/storyteller_web/src/http_server/web_utils/cookie_manager.rs rename to crates/component_subsystem/users/src/utils/session_cookie_manager.rs index c2de649aff..35e096e351 100644 --- a/crates/service/storyteller_web/src/http_server/web_utils/cookie_manager.rs +++ b/crates/component_subsystem/users/src/utils/session_cookie_manager.rs @@ -1,29 +1,35 @@ -use actix_web::cookie::{Cookie, SameSite}; -use actix_web::{HttpRequest, HttpMessage}; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_web::HttpRequest; +use actix_web::cookie::Cookie; use anyhow::anyhow; -use crate::AnyhowResult; +use container_common::anyhow_result::AnyhowResult; use hmac::Hmac; use hmac::NewMac; use jwt::SignWithKey; use jwt::VerifyWithKey; -use log::LevelFilter::Off; -use log::{info, warn}; +use log::warn; use sha2::Sha256; use std::collections::BTreeMap; -use std::ops::Sub; use time::OffsetDateTime; const COOKIE_VERSION : u32 = 1; const SESSION_COOKIE_NAME : &'static str = "session"; +// TODO(echelon,2022-08-29): Make a CryptedCookieManager that this uses. +// TODO(echelon,2022-08-29): Fix how domains and "secure" cookies are handled + #[derive(Clone)] -pub struct CookieManager { +pub struct SessionCookieManager { cookie_domain: String, hmac_secret: String, } -impl CookieManager { +impl SessionCookieManager { pub fn new(cookie_domain: &str, hmac_secret: &str) -> Self { Self { cookie_domain: cookie_domain.to_string(), diff --git a/crates/service/storyteller_web/src/validations/username_reservations.rs b/crates/component_subsystem/users/src/validations/is_reserved_username.rs similarity index 91% rename from crates/service/storyteller_web/src/validations/username_reservations.rs rename to crates/component_subsystem/users/src/validations/is_reserved_username.rs index 5501341162..62d1b0d4f8 100644 --- a/crates/service/storyteller_web/src/validations/username_reservations.rs +++ b/crates/component_subsystem/users/src/validations/is_reserved_username.rs @@ -1,6 +1,6 @@ -use std::collections::HashSet; use crate::RESERVED_SUBSTRINGS; use crate::RESERVED_USERNAMES; +use std::collections::HashSet; pub fn is_reserved_username(username: &str) -> bool { lazy_static! { @@ -40,7 +40,7 @@ fn is_reserved_substring(username: &str) -> bool { #[cfg(test)] mod tests { - use crate::validations::username_reservations::{is_reserved_username}; + use crate::validations::is_reserved_username::is_reserved_username; #[test] fn reserved_usernames() { @@ -62,11 +62,13 @@ mod tests { assert_eq!(is_reserved_username("12345test"), true); assert_eq!(is_reserved_username("test"), true); assert_eq!(is_reserved_username("111vocodes111"), true); + assert_eq!(is_reserved_username("thefakeyousite"), true); } #[test] fn reserved_substrings_with_dashes() { assert_eq!(is_reserved_username("t_e_s_t"), true); assert_eq!(is_reserved_username("-vo-co-de-s--"), true); + assert_eq!(is_reserved_username("fake_you"), true); } } \ No newline at end of file diff --git a/crates/component_subsystem/users/src/validations/mod.rs b/crates/component_subsystem/users/src/validations/mod.rs new file mode 100644 index 0000000000..497e40e8c6 --- /dev/null +++ b/crates/component_subsystem/users/src/validations/mod.rs @@ -0,0 +1,9 @@ +pub mod is_reserved_username; +pub mod validate_passwords; +pub mod validate_profile_cashapp_username; +pub mod validate_profile_discord_username; +pub mod validate_profile_github_username; +pub mod validate_profile_twitch_username; +pub mod validate_profile_twitter_username; +pub mod validate_profile_website_url; +pub mod validate_username; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/validations/passwords.rs b/crates/component_subsystem/users/src/validations/validate_passwords.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/passwords.rs rename to crates/component_subsystem/users/src/validations/validate_passwords.rs diff --git a/crates/service/storyteller_web/src/validations/cashapp_username.rs b/crates/component_subsystem/users/src/validations/validate_profile_cashapp_username.rs similarity index 61% rename from crates/service/storyteller_web/src/validations/cashapp_username.rs rename to crates/component_subsystem/users/src/validations/validate_profile_cashapp_username.rs index 960b087ed9..1f39dc6ea0 100644 --- a/crates/service/storyteller_web/src/validations/cashapp_username.rs +++ b/crates/component_subsystem/users/src/validations/validate_profile_cashapp_username.rs @@ -1,7 +1,6 @@ -use crate::AnyhowResult; use regex::Regex; -pub fn validate_cashapp_username(username: &str) -> Result<(), String> { +pub fn validate_profile_cashapp_username(username: &str) -> Result<(), String> { lazy_static! { static ref CASHAPP_USERNAME_REGEX: Regex = { Regex::new(r"^\$?.{1,20}$").expect("should be valid regex") @@ -38,22 +37,22 @@ pub fn normalize_cashapp_username_for_storage(username: &str) -> String { #[cfg(test)] mod tests { - use crate::validations::cashapp_username::{validate_cashapp_username, normalize_cashapp_username_for_storage}; + use crate::validations::validate_profile_cashapp_username::{validate_profile_cashapp_username, normalize_cashapp_username_for_storage}; #[test] fn valid_cases() { - assert!(validate_cashapp_username("echelon").is_ok()); - assert!(validate_cashapp_username("$echelon").is_ok()); - assert!(validate_cashapp_username("a").is_ok()); - assert!(validate_cashapp_username("12345678901234567890").is_ok()); - assert!(validate_cashapp_username("$12345678901234567890").is_ok()); + assert!(validate_profile_cashapp_username("echelon").is_ok()); + assert!(validate_profile_cashapp_username("$echelon").is_ok()); + assert!(validate_profile_cashapp_username("a").is_ok()); + assert!(validate_profile_cashapp_username("12345678901234567890").is_ok()); + assert!(validate_profile_cashapp_username("$12345678901234567890").is_ok()); } #[test] fn invalid_cases() { - assert!(validate_cashapp_username("").is_err()); - assert!(validate_cashapp_username("$").is_err()); - assert!(validate_cashapp_username("123456789012345678901").is_err()); + assert!(validate_profile_cashapp_username("").is_err()); + assert!(validate_profile_cashapp_username("$").is_err()); + assert!(validate_profile_cashapp_username("123456789012345678901").is_err()); } #[test] diff --git a/crates/service/storyteller_web/src/validations/discord_username.rs b/crates/component_subsystem/users/src/validations/validate_profile_discord_username.rs similarity index 60% rename from crates/service/storyteller_web/src/validations/discord_username.rs rename to crates/component_subsystem/users/src/validations/validate_profile_discord_username.rs index 1b49ce1fcd..23c127e7b3 100644 --- a/crates/service/storyteller_web/src/validations/discord_username.rs +++ b/crates/component_subsystem/users/src/validations/validate_profile_discord_username.rs @@ -1,7 +1,6 @@ -use crate::AnyhowResult; use regex::Regex; -pub fn validate_discord_username(username: &str) -> Result<(), String> { +pub fn validate_profile_discord_username(username: &str) -> Result<(), String> { lazy_static! { static ref DISCORD_USERNAME_REGEX: Regex = { Regex::new(r"^@?[^#@:]{2,32}#[0-9]{4}$").expect("should be valid regex") @@ -25,16 +24,16 @@ pub fn validate_discord_username(username: &str) -> Result<(), String> { #[cfg(test)] mod tests { - use crate::validations::discord_username::validate_discord_username; + use crate::validations::validate_profile_discord_username::validate_profile_discord_username; #[test] fn valid_cases() { - assert!(validate_discord_username("echelon#0001").is_ok()); - assert!(validate_discord_username("@echelon#0001").is_ok()); + assert!(validate_profile_discord_username("echelon#0001").is_ok()); + assert!(validate_profile_discord_username("@echelon#0001").is_ok()); } #[test] fn invalid_cases() { - assert!(validate_discord_username("").is_err()); + assert!(validate_profile_discord_username("").is_err()); } } \ No newline at end of file diff --git a/crates/service/storyteller_web/src/validations/github_username.rs b/crates/component_subsystem/users/src/validations/validate_profile_github_username.rs similarity index 62% rename from crates/service/storyteller_web/src/validations/github_username.rs rename to crates/component_subsystem/users/src/validations/validate_profile_github_username.rs index 1654b887a1..f8df8809d1 100644 --- a/crates/service/storyteller_web/src/validations/github_username.rs +++ b/crates/component_subsystem/users/src/validations/validate_profile_github_username.rs @@ -1,7 +1,6 @@ -use crate::AnyhowResult; use regex::Regex; -pub fn validate_github_username(username: &str) -> Result<(), String> { +pub fn validate_profile_github_username(username: &str) -> Result<(), String> { lazy_static! { // TODO: https://github.com/shinnn/github-username-regex static ref GITHUB_USERNAME_REGEX: Regex = { @@ -26,16 +25,16 @@ pub fn validate_github_username(username: &str) -> Result<(), String> { #[cfg(test)] mod tests { - use crate::validations::github_username::validate_github_username; + use crate::validations::validate_profile_github_username::validate_profile_github_username; #[test] fn valid_cases() { - assert!(validate_github_username("echelon").is_ok()); - assert!(validate_github_username("a-b-c-D-E-F-0-1-2").is_ok()); + assert!(validate_profile_github_username("echelon").is_ok()); + assert!(validate_profile_github_username("a-b-c-D-E-F-0-1-2").is_ok()); } #[test] fn invalid_cases() { - assert!(validate_github_username("").is_err()); + assert!(validate_profile_github_username("").is_err()); } } diff --git a/crates/component_subsystem/users/src/validations/validate_profile_twitch_username.rs b/crates/component_subsystem/users/src/validations/validate_profile_twitch_username.rs new file mode 100644 index 0000000000..e0ecd82d1f --- /dev/null +++ b/crates/component_subsystem/users/src/validations/validate_profile_twitch_username.rs @@ -0,0 +1,42 @@ +use regex::Regex; + +pub fn validate_profile_twitch_username(username: &str) -> Result<(), String> { + lazy_static! { + static ref TWITCH_USERNAME_REGEX: Regex = { + Regex::new(r"^[a-zA-Z0-9]{4,25}$").expect("should be valid regex") + }; + } + + if username.len() < 4 { + return Err("twitch username is too short".to_string()); + } + + if username.len() > 25 { + return Err("twitch username is too long".to_string()); + } + + if !TWITCH_USERNAME_REGEX.is_match(username) { + return Err("twitch username invalid".to_string()); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::validations::validate_profile_twitch_username::validate_profile_twitch_username; + + #[test] + fn valid_cases() { + assert!(validate_profile_twitch_username("echelon").is_ok()); + assert!(validate_profile_twitch_username("1234").is_ok()); + assert!(validate_profile_twitch_username("1234567890123456789012345").is_ok()); + } + + #[test] + fn invalid_cases() { + assert!(validate_profile_twitch_username("").is_err()); + assert!(validate_profile_twitch_username("@=[]@34!").is_err()); + assert!(validate_profile_twitch_username("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err()); + } +} \ No newline at end of file diff --git a/crates/service/storyteller_web/src/validations/twitter_username.rs b/crates/component_subsystem/users/src/validations/validate_profile_twitter_username.rs similarity index 50% rename from crates/service/storyteller_web/src/validations/twitter_username.rs rename to crates/component_subsystem/users/src/validations/validate_profile_twitter_username.rs index 3deed67fa8..e324d49be4 100644 --- a/crates/service/storyteller_web/src/validations/twitter_username.rs +++ b/crates/component_subsystem/users/src/validations/validate_profile_twitter_username.rs @@ -1,8 +1,6 @@ -use crate::AnyhowResult; use regex::Regex; - -pub fn validate_twitter_username(username: &str) -> Result<(), String> { +pub fn validate_profile_twitter_username(username: &str) -> Result<(), String> { lazy_static! { static ref TWITTER_USERNAME_REGEX: Regex = { Regex::new("^@?[A-Za-z0-9_]{4,15}$").expect("should be valid regex") @@ -39,29 +37,29 @@ pub fn normalize_twitter_username_for_storage(username: &str) -> String { #[cfg(test)] mod tests { - use crate::validations::twitter_username::{validate_twitter_username, normalize_twitter_username_for_storage}; + use crate::validations::validate_profile_twitter_username::{validate_profile_twitter_username, normalize_twitter_username_for_storage}; #[test] fn valid_cases() { - assert!(validate_twitter_username("@echelon").is_ok()); - assert!(validate_twitter_username("echelon").is_ok()); - assert!(validate_twitter_username("four").is_ok()); - assert!(validate_twitter_username("@four").is_ok()); - assert!(validate_twitter_username("123456789012345").is_ok()); - assert!(validate_twitter_username("@123456789012345").is_ok()); - assert!(validate_twitter_username("a_A_b_B_c_C_d_D").is_ok()); - assert!(validate_twitter_username("@a_A_b_B_c_C_d_D").is_ok()); + assert!(validate_profile_twitter_username("@echelon").is_ok()); + assert!(validate_profile_twitter_username("echelon").is_ok()); + assert!(validate_profile_twitter_username("four").is_ok()); + assert!(validate_profile_twitter_username("@four").is_ok()); + assert!(validate_profile_twitter_username("123456789012345").is_ok()); + assert!(validate_profile_twitter_username("@123456789012345").is_ok()); + assert!(validate_profile_twitter_username("a_A_b_B_c_C_d_D").is_ok()); + assert!(validate_profile_twitter_username("@a_A_b_B_c_C_d_D").is_ok()); } #[test] fn invalid_cases() { - assert!(validate_twitter_username("").is_err()); - assert!(validate_twitter_username("@").is_err()); - assert!(validate_twitter_username(" ").is_err()); - assert!(validate_twitter_username("!!!!!!!!!!!").is_err()); - assert!(validate_twitter_username("a").is_err()); - assert!(validate_twitter_username("@a").is_err()); - assert!(validate_twitter_username("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err()); + assert!(validate_profile_twitter_username("").is_err()); + assert!(validate_profile_twitter_username("@").is_err()); + assert!(validate_profile_twitter_username(" ").is_err()); + assert!(validate_profile_twitter_username("!!!!!!!!!!!").is_err()); + assert!(validate_profile_twitter_username("a").is_err()); + assert!(validate_profile_twitter_username("@a").is_err()); + assert!(validate_profile_twitter_username("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err()); } #[test] diff --git a/crates/service/storyteller_web/src/validations/website_url.rs b/crates/component_subsystem/users/src/validations/validate_profile_website_url.rs similarity index 56% rename from crates/service/storyteller_web/src/validations/website_url.rs rename to crates/component_subsystem/users/src/validations/validate_profile_website_url.rs index b3b5614b49..7e701dac78 100644 --- a/crates/service/storyteller_web/src/validations/website_url.rs +++ b/crates/component_subsystem/users/src/validations/validate_profile_website_url.rs @@ -1,7 +1,6 @@ -use crate::AnyhowResult; use regex::Regex; -pub fn validate_website_url(website_url: &str) -> Result<(), String> { +pub fn validate_profile_website_url(website_url: &str) -> Result<(), String> { lazy_static! { static ref WEBSITE_URL_REGEX: Regex = { Regex::new(r"^(https?:\\/\\/)?.*$").expect("should be valid regex") @@ -25,17 +24,17 @@ pub fn validate_website_url(website_url: &str) -> Result<(), String> { #[cfg(test)] mod tests { - use crate::validations::website_url::validate_website_url; + use crate::validations::validate_profile_website_url::validate_profile_website_url; #[test] fn valid_cases() { - assert!(validate_website_url("http://vo.codes").is_ok()); - assert!(validate_website_url("https://vocodes.com").is_ok()); - assert!(validate_website_url("www.zombo.com").is_ok()); + assert!(validate_profile_website_url("http://vo.codes").is_ok()); + assert!(validate_profile_website_url("https://vocodes.com").is_ok()); + assert!(validate_profile_website_url("www.zombo.com").is_ok()); } #[test] fn invalid_cases() { - assert!(validate_website_url("a").is_err()); + assert!(validate_profile_website_url("a").is_err()); } } diff --git a/crates/service/storyteller_web/src/validations/username.rs b/crates/component_subsystem/users/src/validations/validate_username.rs similarity index 64% rename from crates/service/storyteller_web/src/validations/username.rs rename to crates/component_subsystem/users/src/validations/validate_username.rs index c823959b7f..1059a259e2 100644 --- a/crates/service/storyteller_web/src/validations/username.rs +++ b/crates/component_subsystem/users/src/validations/validate_username.rs @@ -1,4 +1,3 @@ -use crate::AnyhowResult; use regex::Regex; pub fn validate_username(username: &str) -> Result<(), String> { @@ -23,31 +22,9 @@ pub fn validate_username(username: &str) -> Result<(), String> { Ok(()) } -/*pub fn validate_display_name(display_name: &str) -> Result<(), String> { - lazy_static! { - static ref DISPLAY_NAME_REGEX: Regex = { - Regex::new(r"^\w{3,16}$").expect("should be valid regex") - }; - } - - if username.len() < 3 { - return Err("display name is too short".to_string()); - } - - if username.len() > 16 { - return Err("display name is too long".to_string()); - } - - if !DISPLAY_NAME_REGEX.is_match(username) { - return Err("invalid display name".to_string()); - } - - Ok(()) -}*/ - #[cfg(test)] mod tests { - use crate::validations::username::validate_username; + use crate::validations::validate_username::validate_username; #[test] fn test_valid_usernames() { diff --git a/crates/lib/container_common/src/filesystem/check_directory_exists.rs b/crates/lib/container_common/src/filesystem/check_directory_exists.rs index 7b86b92850..c75aec81e1 100644 --- a/crates/lib/container_common/src/filesystem/check_directory_exists.rs +++ b/crates/lib/container_common/src/filesystem/check_directory_exists.rs @@ -1,9 +1,5 @@ use anyhow::bail; use crate::anyhow_result::AnyhowResult; -use log::info; -use log::warn; -use std::fs; -use std::path::Path; use std::path::PathBuf; pub fn check_directory_exists(path: &PathBuf) -> AnyhowResult<()> { diff --git a/crates/lib/container_common/src/filesystem/check_file_exists.rs b/crates/lib/container_common/src/filesystem/check_file_exists.rs index 1932422a27..d7619aa77d 100644 --- a/crates/lib/container_common/src/filesystem/check_file_exists.rs +++ b/crates/lib/container_common/src/filesystem/check_file_exists.rs @@ -1,9 +1,5 @@ use anyhow::bail; use crate::anyhow_result::AnyhowResult; -use log::info; -use log::warn; -use std::fs; -use std::path::Path; use std::path::PathBuf; pub fn check_file_exists(path: &PathBuf) -> AnyhowResult<()> { diff --git a/crates/lib/container_common/src/filesystem/safe_delete_temp_directory.rs b/crates/lib/container_common/src/filesystem/safe_delete_temp_directory.rs index a796358347..ac0a9b6171 100644 --- a/crates/lib/container_common/src/filesystem/safe_delete_temp_directory.rs +++ b/crates/lib/container_common/src/filesystem/safe_delete_temp_directory.rs @@ -1,10 +1,7 @@ -use anyhow::bail; -use crate::anyhow_result::AnyhowResult; use log::info; use log::warn; use std::fs; use std::path::Path; -use std::path::PathBuf; pub fn safe_delete_temp_directory>(directory_path: P) { // NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk... diff --git a/crates/lib/database_queries/src/helpers/boolean_converters.rs b/crates/lib/database_queries/src/helpers/boolean_converters.rs index 45d122f3c7..8e24071789 100644 --- a/crates/lib/database_queries/src/helpers/boolean_converters.rs +++ b/crates/lib/database_queries/src/helpers/boolean_converters.rs @@ -15,6 +15,16 @@ pub fn nullable_i8_to_bool(value: Option, default_value: bool) -> bool { .unwrap_or(default_value) } +pub fn nullable_i8_to_bool_default_false(value: Option) -> bool { + match value { + None => false, + Some(v) => match v { + 0 => false, + _ => true, + } + } +} + /// Bool conversion, but retain nulls. pub fn nullable_i8_to_optional_bool(value: Option) -> Option { value.map(|v| i8_to_bool(v)) @@ -22,11 +32,7 @@ pub fn nullable_i8_to_optional_bool(value: Option) -> Option { #[cfg(test)] mod tests { - use crate::helpers::boolean_converters::{ - i8_to_bool, - nullable_i8_to_bool, - nullable_i8_to_optional_bool - }; + use crate::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_bool, nullable_i8_to_bool_default_false, nullable_i8_to_optional_bool}; #[test] fn test_i8_to_bool() { @@ -52,6 +58,16 @@ mod tests { assert_eq!(nullable_i8_to_bool(Some(-100), true), true); } + #[test] + fn test_nullable_i8_to_bool_default_false() { + assert_eq!(nullable_i8_to_bool_default_false(None), false); + assert_eq!(nullable_i8_to_bool_default_false(Some(0)), false); + + assert_eq!(nullable_i8_to_bool_default_false(Some(1)), true); + assert_eq!(nullable_i8_to_bool_default_false(Some(100)), true); + assert_eq!(nullable_i8_to_bool_default_false(Some(-100)), true); + } + #[test] fn test_nullable_i8_to_optional_bool() { assert_eq!(nullable_i8_to_optional_bool(None), None); diff --git a/crates/lib/database_queries/src/queries/users/create_account.rs b/crates/lib/database_queries/src/queries/users/create_account.rs new file mode 100644 index 0000000000..d28f70107b --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/create_account.rs @@ -0,0 +1,114 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use crate::tokens::Tokens; +use log::warn; +use sqlx::MySqlPool; +use sqlx::error::Error::Database; + +pub struct CreateAccountArgs<'a> { + pub username: &'a str, + pub display_name: &'a str, + pub email_address: &'a str, + pub email_gravatar_hash: &'a str, + pub password_hash: &'a str, + pub ip_address: &'a str, +} + +pub struct CreateAccountSuccessResult { + pub user_token: String, + pub user_id: u64, +} + +pub enum CreateAccountError { + EmailIsTaken, + UsernameIsTaken, + DatabaseError, + OtherError, +} + +pub async fn create_account( + mysql_pool: &MySqlPool, + args: CreateAccountArgs<'_>, +) -> Result +{ + const INITIAL_PROFILE_MARKDOWN : &'static str = ""; + const INITIAL_PROFILE_RENDERED_HTML : &'static str = ""; + const INITIAL_USER_ROLE: &'static str = "user"; + + let user_token = Tokens::new_user() + .map_err(|_e| { + warn!("problem creating user token"); + CreateAccountError::OtherError + })?; + + let query_result = sqlx::query!( + r#" +INSERT INTO users ( + token, + username, + display_name, + email_address, + email_gravatar_hash, + profile_markdown, + profile_rendered_html, + user_role_slug, + password_hash, + ip_address_creation, + ip_address_last_login, + ip_address_last_update +) +VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) + "#, + &user_token, + args.username, + args.display_name, + args.email_address, + args.email_gravatar_hash, + INITIAL_PROFILE_MARKDOWN, + INITIAL_PROFILE_RENDERED_HTML, + INITIAL_USER_ROLE, + args.password_hash, + args.ip_address, + args.ip_address, + args.ip_address, + ) + .execute(mysql_pool) + .await; + + let record_id = match query_result { + Ok(res) => { + res.last_insert_id() + }, + Err(err) => { + warn!("New user creation DB error: {:?}", err); + + // NB: SQLSTATE[23000]: Integrity constraint violation + // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) + match err { + Database(err) => { + let maybe_code = err.code().map(|c| c.into_owned()); + match maybe_code.as_deref() { + Some("23000") => { + if err.message().contains("username") { + return Err(CreateAccountError::UsernameIsTaken); + } else if err.message().contains("email_address") { + return Err(CreateAccountError::EmailIsTaken); + } + } + _ => {}, + } + }, + _ => {}, + } + return Err(CreateAccountError::DatabaseError); + } + }; + + Ok(CreateAccountSuccessResult { + user_token, + user_id: record_id, + }) +} diff --git a/crates/lib/database_queries/src/queries/users/mod.rs b/crates/lib/database_queries/src/queries/users/mod.rs index a4b95c51c7..68a2723295 100644 --- a/crates/lib/database_queries/src/queries/users/mod.rs +++ b/crates/lib/database_queries/src/queries/users/mod.rs @@ -1,5 +1,6 @@ -pub mod get_user_profile_by_username; +pub mod create_account; pub mod list_users_query_builder; pub mod user_badges; +pub mod user_profiles; pub mod user_roles; pub mod user_sessions; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_account_holder.rs b/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_account_holder.rs new file mode 100644 index 0000000000..b801ae7fb6 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_account_holder.rs @@ -0,0 +1,72 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use sqlx::MySqlPool; +use container_common::anyhow_result::AnyhowResult; + +pub struct Args<'a> { + pub user_token: &'a str, + + pub profile_markdown: Option<&'a str>, + pub profile_html: Option<&'a str>, + + pub discord_username: Option<&'a str>, + pub twitter_username: Option<&'a str>, + pub cashapp_username: Option<&'a str>, + pub github_username: Option<&'a str>, + pub twitch_username: Option<&'a str>, + pub website_url: Option<&'a str>, + + pub preferred_tts_result_visibility: &'a str, + pub preferred_w2l_result_visibility: &'a str, + + // We need to store the IP address details for non-mods. + pub ip_address: &'a str, +} + +pub async fn edit_user_profile_as_account_holder( + mysql_pool: &MySqlPool, + args: Args<'_>, +) -> AnyhowResult<()> +{ + let _result = sqlx::query!( + r#" +UPDATE users +SET + profile_markdown = ?, + profile_rendered_html = ?, + preferred_tts_result_visibility = ?, + preferred_w2l_result_visibility = ?, + discord_username = ?, + twitter_username = ?, + twitch_username = ?, + github_username = ?, + cashapp_username = ?, + website_url = ?, + ip_address_last_update = ?, + version = version + 1 + +WHERE users.token = ? +LIMIT 1 + "#, + args.profile_markdown, + args.profile_html, + args.preferred_tts_result_visibility, + args.preferred_w2l_result_visibility, + args.discord_username, + args.twitter_username, + args.twitch_username, + args.github_username, + args.cashapp_username, + args.website_url, + args.ip_address, + args.user_token, + ) + .execute(mysql_pool) + .await?; + + Ok(()) +} + diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_mod.rs b/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_mod.rs new file mode 100644 index 0000000000..9cba655690 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_profiles/edit_user_profile_as_mod.rs @@ -0,0 +1,61 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use sqlx::MySqlPool; +use container_common::anyhow_result::AnyhowResult; + +pub struct Args<'a> { + pub user_token: &'a str, + + pub profile_markdown: Option<&'a str>, + pub profile_html: Option<&'a str>, + + pub discord_username: Option<&'a str>, + pub twitter_username: Option<&'a str>, + pub cashapp_username: Option<&'a str>, + pub github_username: Option<&'a str>, + pub twitch_username: Option<&'a str>, + pub website_url: Option<&'a str>, +} + +/// Mods can edit user profiles, but shouldn't be able to change preferences +/// We won't set the last edit IP address either. +pub async fn edit_user_profile_as_mod( + mysql_pool: &MySqlPool, + args: Args<'_>, +) -> AnyhowResult<()> +{ + let _result = sqlx::query!( + r#" +UPDATE users +SET + profile_markdown = ?, + profile_rendered_html = ?, + discord_username = ?, + twitter_username = ?, + twitch_username = ?, + github_username = ?, + cashapp_username = ?, + website_url = ?, + version = version + 1 + +WHERE users.token = ? +LIMIT 1 + "#, + args.profile_markdown, + args.profile_html, + args.discord_username, + args.twitter_username, + args.twitch_username, + args.github_username, + args.cashapp_username, + args.website_url, + args.user_token, + ) + .execute(mysql_pool) + .await?; + + Ok(()) +} diff --git a/crates/lib/database_queries/src/queries/users/get_user_profile_by_username.rs b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs similarity index 100% rename from crates/lib/database_queries/src/queries/users/get_user_profile_by_username.rs rename to crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/mod.rs b/crates/lib/database_queries/src/queries/users/user_profiles/mod.rs new file mode 100644 index 0000000000..0167208cd4 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_profiles/mod.rs @@ -0,0 +1,3 @@ +pub mod edit_user_profile_as_account_holder; +pub mod edit_user_profile_as_mod; +pub mod get_user_profile_by_username; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/delete_session.rs b/crates/lib/database_queries/src/queries/users/user_sessions/delete_session.rs new file mode 100644 index 0000000000..117f5a5772 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/delete_session.rs @@ -0,0 +1,19 @@ +use container_common::anyhow_result::AnyhowResult; +use sqlx::MySqlPool; + +pub async fn delete_session(session_token: &str, mysql_pool: &MySqlPool) -> AnyhowResult<()> { + let query_result = sqlx::query!( + r#" +UPDATE user_sessions +SET deleted_at = CURRENT_TIMESTAMP() +WHERE + token = ? + AND deleted_at IS NULL + "#, + session_token.to_string(), + ) + .execute(mysql_pool) + .await; + + Ok(()) +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token.rs new file mode 100644 index 0000000000..6fe4e3c8c9 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token.rs @@ -0,0 +1,231 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] + +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::column_types::record_visibility::RecordVisibility; +use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool, nullable_i8_to_bool_default_false}; +use log::warn; +use sqlx::MySql; +use sqlx::pool::PoolConnection; + +pub struct SessionUserRecord { + pub user_token: String, + pub username: String, + pub display_name: String, + + pub email_address: String, + pub email_confirmed: bool, + pub email_gravatar_hash: String, + + // ===== PREFERENCES ===== // + + pub disable_gravatar: bool, + pub auto_play_audio_preference: Option, + pub preferred_tts_result_visibility: RecordVisibility, + pub preferred_w2l_result_visibility: RecordVisibility, + pub auto_play_video_preference: Option, + + // ===== ROLE ===== // + + pub user_role_slug: String, + pub is_banned: bool, + + // ===== PERMISSIONS FLAGS ===== // + + // Usage + 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 + 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 + 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, +} + +pub async fn get_session_by_token( + mysql_connection: &mut PoolConnection, + session_token: &str, +) -> AnyhowResult> { + + // NB: Lookup failure is Err(RowNotFound). + let maybe_user_record = sqlx::query_as!( + SessionUserRawDbRecord, + r#" +SELECT + users.token as user_token, + users.username, + users.display_name, + + users.email_address, + users.email_confirmed, + users.email_gravatar_hash, + + users.disable_gravatar, + users.auto_play_audio_preference, + users.auto_play_video_preference, + users.preferred_tts_result_visibility as `preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility`, + users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: crate::column_types::record_visibility::RecordVisibility`, + + users.user_role_slug, + users.is_banned, + + user_roles.can_use_tts, + user_roles.can_use_w2l, + user_roles.can_delete_own_tts_results, + user_roles.can_delete_own_w2l_results, + user_roles.can_delete_own_account, + + user_roles.can_upload_tts_models, + user_roles.can_upload_w2l_templates, + user_roles.can_delete_own_tts_models, + user_roles.can_delete_own_w2l_templates, + + user_roles.can_approve_w2l_templates, + user_roles.can_edit_other_users_profiles, + user_roles.can_edit_other_users_tts_models, + user_roles.can_edit_other_users_w2l_templates, + user_roles.can_delete_other_users_tts_models, + user_roles.can_delete_other_users_tts_results, + user_roles.can_delete_other_users_w2l_templates, + user_roles.can_delete_other_users_w2l_results, + user_roles.can_ban_users, + user_roles.can_delete_users + +FROM users +LEFT OUTER JOIN user_sessions + ON users.token = user_sessions.user_token +LEFT OUTER JOIN user_roles + ON users.user_role_slug = user_roles.slug +WHERE user_sessions.token = ? + AND user_sessions.deleted_at IS NULL + AND users.user_deleted_at IS NULL + AND users.mod_deleted_at IS NULL + "#, + session_token, + ) + .fetch_one(mysql_connection) + .await; // TODO: This will return error if it doesn't exist + + match maybe_user_record { + Ok(raw_user_record) => { + let result_user_record = SessionUserRecord { + user_token: raw_user_record.user_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, + // Preference + disable_gravatar: i8_to_bool(raw_user_record.disable_gravatar), + auto_play_audio_preference: nullable_i8_to_optional_bool(raw_user_record.auto_play_audio_preference), + auto_play_video_preference: nullable_i8_to_optional_bool(raw_user_record.auto_play_video_preference), + user_role_slug: raw_user_record.user_role_slug, + preferred_tts_result_visibility: raw_user_record.preferred_tts_result_visibility, + preferred_w2l_result_visibility: raw_user_record.preferred_w2l_result_visibility, + + is_banned: i8_to_bool(raw_user_record.is_banned), + + // Usage + can_use_tts: nullable_i8_to_bool_default_false(raw_user_record.can_use_tts), + can_use_w2l: nullable_i8_to_bool_default_false(raw_user_record.can_use_w2l), + can_delete_own_tts_results: nullable_i8_to_bool_default_false(raw_user_record.can_delete_own_tts_results), + can_delete_own_w2l_results: nullable_i8_to_bool_default_false(raw_user_record.can_delete_own_w2l_results), + can_delete_own_account: nullable_i8_to_bool_default_false(raw_user_record.can_delete_own_account), + // Contribution + can_upload_tts_models: nullable_i8_to_bool_default_false(raw_user_record.can_upload_tts_models), + can_upload_w2l_templates: nullable_i8_to_bool_default_false(raw_user_record.can_upload_w2l_templates), + can_delete_own_tts_models: nullable_i8_to_bool_default_false(raw_user_record.can_delete_own_tts_models), + can_delete_own_w2l_templates: nullable_i8_to_bool_default_false(raw_user_record.can_delete_own_w2l_templates), + // Moderation + can_approve_w2l_templates: nullable_i8_to_bool_default_false(raw_user_record.can_approve_w2l_templates), + can_edit_other_users_profiles: nullable_i8_to_bool_default_false(raw_user_record.can_edit_other_users_profiles), + can_edit_other_users_tts_models: nullable_i8_to_bool_default_false(raw_user_record.can_edit_other_users_tts_models), + can_edit_other_users_w2l_templates: nullable_i8_to_bool_default_false(raw_user_record.can_edit_other_users_w2l_templates), + can_delete_other_users_tts_models: nullable_i8_to_bool_default_false(raw_user_record.can_delete_other_users_tts_models), + can_delete_other_users_tts_results: nullable_i8_to_bool_default_false(raw_user_record.can_delete_other_users_tts_results), + can_delete_other_users_w2l_templates: nullable_i8_to_bool_default_false(raw_user_record.can_delete_other_users_w2l_templates), + can_delete_other_users_w2l_results: nullable_i8_to_bool_default_false(raw_user_record.can_delete_other_users_w2l_results), + can_ban_users: nullable_i8_to_bool_default_false(raw_user_record.can_ban_users), + can_delete_users: nullable_i8_to_bool_default_false(raw_user_record.can_delete_users) , + }; + + Ok(Some(result_user_record)) + }, + Err(err) => { + match err { + RowNotFound => { + warn!("Valid cookie; invalid session: {}", session_token); + Ok(None) + }, + _ => { + warn!("Session query error: {:?}", err); + Err(anyhow!("session query error: {:?}", err)) + } + } + } + } + +} + +struct SessionUserRawDbRecord { + user_token: String, + username: String, + display_name: String, + + email_address: String, + email_confirmed: i8, + email_gravatar_hash: String, + + disable_gravatar: i8, + auto_play_audio_preference: Option, + auto_play_video_preference: Option, + preferred_tts_result_visibility: RecordVisibility, + preferred_w2l_result_visibility: RecordVisibility, + + user_role_slug: String, + is_banned: i8, + + // NB: These are `Option` due to the JOIN not being compile-time assured. + // Usage + can_use_tts: Option, + can_use_w2l: Option, + can_delete_own_tts_results: Option, + can_delete_own_w2l_results: Option, + can_delete_own_account: Option, + + // Contribution + can_upload_tts_models: Option, + can_upload_w2l_templates: Option, + can_delete_own_tts_models: Option, + can_delete_own_w2l_templates: Option, + + // Moderation + can_approve_w2l_templates: Option, + can_edit_other_users_profiles: Option, + can_edit_other_users_tts_models: Option, + can_edit_other_users_w2l_templates: Option, + can_delete_other_users_tts_models: Option, + can_delete_other_users_tts_results: Option, + can_delete_other_users_w2l_templates: Option, + can_delete_other_users_w2l_results: Option, + can_ban_users: Option, + can_delete_users: Option, +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token_light.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token_light.rs new file mode 100644 index 0000000000..eaa947757c --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_session_by_token_light.rs @@ -0,0 +1,51 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] + +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use log::warn; +use sqlx::MySql; +use sqlx::pool::PoolConnection; + +pub struct SessionRecord { + pub session_token: String, + pub user_token: String, +} +pub async fn get_session_by_token_light( + mysql_connection: &mut PoolConnection, + session_token: &str, +) -> AnyhowResult> { + + // NB: Lookup failure is Err(RowNotFound). + let maybe_session_record = sqlx::query_as!( + SessionRecord, + r#" +SELECT + token as session_token, + user_token +FROM user_sessions +WHERE token = ? +AND deleted_at IS NULL + "#, + session_token, + ) + .fetch_one(mysql_connection) + .await; + + match maybe_session_record { + Ok(session_record) => Ok(Some(session_record)), + Err(err) => { + match err { + RowNotFound => { + warn!("Valid cookie; invalid session: {}", session_token); + Ok(None) + }, + _ => { + warn!("Session query error: {:?}", err); + Err(anyhow!("session query error: {:?}", err)) + } + } + } + } +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_email.rs b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_email.rs new file mode 100644 index 0000000000..da084afa5f --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_email.rs @@ -0,0 +1,21 @@ +use container_common::anyhow_result::AnyhowResult; +use crate::queries::users::user_sessions::lookup_user_for_login_result::UserRecordForLogin; +use sqlx::MySqlPool; + +pub async fn lookup_user_for_login_by_email(email: &str, pool: &MySqlPool) -> AnyhowResult { + // NB: Lookup failure is Err(RowNotFound). + let record = sqlx::query_as!( + UserRecordForLogin, + r#" +SELECT token, username, email_address, password_hash, is_banned +FROM users +WHERE email_address = ? +LIMIT 1 + "#, + email.to_string(), + ) + .fetch_one(pool) + .await?; + + Ok(record) +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_username.rs b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_username.rs new file mode 100644 index 0000000000..b4bcd7b530 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_by_username.rs @@ -0,0 +1,22 @@ +use container_common::anyhow_result::AnyhowResult; +use crate::queries::users::user_sessions::lookup_user_for_login_result::UserRecordForLogin; +use sqlx::MySqlPool; + +pub async fn lookup_user_for_login_by_username(username: &str, pool: &MySqlPool) -> AnyhowResult +{ + // NB: Lookup failure is Err(RowNotFound). + let record = sqlx::query_as!( + UserRecordForLogin, + r#" +SELECT token, username, email_address, password_hash, is_banned +FROM users +WHERE username = ? +LIMIT 1 + "#, + username, + ) + .fetch_one(pool) + .await?; + + Ok(record) +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_result.rs b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_result.rs new file mode 100644 index 0000000000..764d366805 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_sessions/lookup_user_for_login_result.rs @@ -0,0 +1,9 @@ +/// Shared result type for user lookups for login (via email or username) +#[derive(Debug)] +pub struct UserRecordForLogin { + pub token: String, + pub username: String, + pub email_address: String, + pub password_hash: Vec, + pub is_banned: i8, +} diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/mod.rs b/crates/lib/database_queries/src/queries/users/user_sessions/mod.rs index 6a3f6dcb74..774f200ca2 100644 --- a/crates/lib/database_queries/src/queries/users/user_sessions/mod.rs +++ b/crates/lib/database_queries/src/queries/users/user_sessions/mod.rs @@ -1 +1,7 @@ -pub mod create_session; \ No newline at end of file +pub mod create_session; +pub mod delete_session; +pub mod get_session_by_token; +pub mod get_session_by_token_light; +pub mod lookup_user_for_login_by_email; +pub mod lookup_user_for_login_by_username; +pub mod lookup_user_for_login_result; \ No newline at end of file diff --git a/crates/lib/user_input_common/Cargo.toml b/crates/lib/user_input_common/Cargo.toml index 2c1d369bf4..5c126932b1 100644 --- a/crates/lib/user_input_common/Cargo.toml +++ b/crates/lib/user_input_common/Cargo.toml @@ -21,5 +21,6 @@ container_common = { path = "../container_common" } anyhow = "1.0.40" log = "0.4.14" once_cell = "1.9.0" +pulldown-cmark = "0.8.0" # Markdown regex = "1.5.4" unicode-segmentation = "1.7.1" diff --git a/crates/lib/user_input_common/src/lib.rs b/crates/lib/user_input_common/src/lib.rs index 4439295579..d104ac4f92 100644 --- a/crates/lib/user_input_common/src/lib.rs +++ b/crates/lib/user_input_common/src/lib.rs @@ -11,3 +11,5 @@ pub (crate) const BANNED_SLURS : &'static str = include_str!("../../../../db/ban pub mod check_for_slurs; pub mod latin_alphabet; +pub mod markdown_to_html; +pub mod validate_user_provided_ip_address; diff --git a/crates/service/storyteller_web/src/util/markdown_to_html.rs b/crates/lib/user_input_common/src/markdown_to_html.rs similarity index 94% rename from crates/service/storyteller_web/src/util/markdown_to_html.rs rename to crates/lib/user_input_common/src/markdown_to_html.rs index 2dccc6cafb..dbc4aca789 100644 --- a/crates/service/storyteller_web/src/util/markdown_to_html.rs +++ b/crates/lib/user_input_common/src/markdown_to_html.rs @@ -1,4 +1,3 @@ -use container_common::anyhow_result::AnyhowResult; use pulldown_cmark::{Options, Parser, html}; pub fn markdown_to_html(markdown_input: &str) -> String { @@ -27,7 +26,7 @@ pub fn markdown_to_html(markdown_input: &str) -> String { #[cfg(test)] mod tests { - use crate::util::markdown_to_html::markdown_to_html; + use crate::markdown_to_html::markdown_to_html; #[test] fn handles_markdown() { @@ -56,5 +55,4 @@ mod tests { assert_eq!(&markdown_to_html(""), "

<script>alert();</script>

\n"); } - } diff --git a/crates/lib/user_input_common/src/validate_user_provided_ip_address.rs b/crates/lib/user_input_common/src/validate_user_provided_ip_address.rs new file mode 100644 index 0000000000..5b8749d9d7 --- /dev/null +++ b/crates/lib/user_input_common/src/validate_user_provided_ip_address.rs @@ -0,0 +1,42 @@ +use once_cell::sync::Lazy; +use regex::Regex; + +const IP_ADDRESS_REGEX : Lazy = Lazy::new(|| { + // TODO: This doesn't require valid IP addresses. Just a number/dot format. + Regex::new(r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$").expect("should be valid regex") +}); + +/// Do not use this for IP addresses from requests. +/// Only for moderator-input IP addresses +pub fn validate_user_provided_ip_address(ip_address: &str) -> Result<(), String> { + if !IP_ADDRESS_REGEX.is_match(ip_address) { + return Err("ip address is invalid".to_string()); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::validate_user_provided_ip_address::validate_user_provided_ip_address; + + #[test] + fn valid_cases() { + assert!(validate_user_provided_ip_address("0.0.0.0").is_ok()); + assert!(validate_user_provided_ip_address("1.1.1.1").is_ok()); + assert!(validate_user_provided_ip_address("1.2.3.4").is_ok()); + assert!(validate_user_provided_ip_address("111.222.3.0").is_ok()); + assert!(validate_user_provided_ip_address("127.0.0.1").is_ok()); + assert!(validate_user_provided_ip_address("255.255.255.255").is_ok()); + } + + #[test] + fn invalid_cases() { + assert!(validate_user_provided_ip_address(" 255.255.255.255 ").is_err()); + assert!(validate_user_provided_ip_address("").is_err()); + assert!(validate_user_provided_ip_address(".1.1.1.1").is_err()); + assert!(validate_user_provided_ip_address("1.1.1").is_err()); + assert!(validate_user_provided_ip_address("127.0.0.1notvalid").is_err()); + assert!(validate_user_provided_ip_address("127.127.127.127.").is_err()); + } +} diff --git a/crates/service/image_generation_service/Cargo.toml b/crates/service/image_generation_service/Cargo.toml new file mode 100644 index 0000000000..90948851b9 --- /dev/null +++ b/crates/service/image_generation_service/Cargo.toml @@ -0,0 +1,126 @@ +[package] +publish = false +name = "image-generation-service" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] + +# Main server monolith +[[bin]] +name = "image-generation-service" +path = "src/main.rs" + +[dependencies] + +# Internal +config = { path = "../../lib/config" } +container_common = { path = "../../lib/container_common" } +database_queries = { path = "../../lib/database_queries" } +google_drive_common = { path = "../../lib/google_drive_common" } +http_server_common = { path = "../../lib/http_server_common" } +jobs_common = { path = "../../lib/jobs_common" } +memory_caching = { path = "../../lib/memory_caching" } +redis_common = { path = "../../lib/redis_common" } +reusable_types = { path = "../../lib/reusable_types" } +storage_buckets_common = { path = "../../lib/storage_buckets_common" } +tts_common = { path = "../../lib/tts_common" } +twitch_common = { path = "../../lib/twitch_common" } +user_input_common = { path = "../../lib/user_input_common" } + +# External +anyhow = "1.0.40" +base64 = "0.13.0" +chrono = { version = "0.4.19", features = ["serde"] } +derive_more = "0.99.14" +dotenv = "0.15.0" +easyenv = "0.0.6" +hex = "0.4.3" +hostname = "0.3.1" +lazy_static = "1.4.0" +lexical-sort = "0.3.1" +lfu = "0.2.5" +log = "0.4.14" +once_cell = "1.9.0" +rand = "0.8.3" +regex = "1.5.4" +time = "0.2.26" +uuid = { version = "0.8.2", features = ["v4"] } + +# Language tag parsing +# TODO: Move to language support crate +language-tags = "0.3.2" # Parsing single tags richly + +# HTTP +# TODO/NB: This is just for Twitch, but we might need it later. +reqwest = { version = "0.11.4" } +native-tls = "0.2.8" + +# Runtime +# NB: We switched to Tokio from `async-std` for broader ecosystem support. +tokio = { version = "1.6.0", features = ["macros"] } + +# Futures +futures = { version = "0.3.17", features = ["thread-pool"] } # feature for ThreadPool +futures-util = "0.3.9" +futures_old_for_limiter = { package = "futures", version = "0.1.29" } # Older version of futures for `limiter` crate + +# MySql Database +# "offline" supports typesafe builds without a live database +# "rustls" doesn't appear to work with IP addresses, per: +# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828 +sqlx = { version = "0.5", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] } + +# Redis Database +limitation = "0.1.1" # Redis rate limiter +r2d2_redis = "0.14.0" + +# In-memory caching +lru_time_cache = "0.11.11" + +# Files +data-encoding = "2.3.2" +ring = "0.17.0-alpha.10" +rust-s3 = { version = "0.28", features = ["never-encode-slash"] } +infer = "0.5.0" + +subprocess = "0.2.6" +tempdir = "0.3.7" + +actix = "0.12.0" +actix-cors = "0.6.0-beta.3" +actix-files= "0.6.0-beta.8" +actix-http = "3.0.0-beta.11" +actix-multipart = "0.4.0-beta.7" +actix-rt = "2.4.0" +actix-service = "2.0.1" +actix-utils = "3.0.0" +actix-web = "4.0.0-beta.10" +actix-web-actors = "4.0.0-beta.7" +jwt = "0.13.0" + +# NB: Hyper is just for HTTP client use in jobs. I'd use Actix client, but I need tokio runtime +# NB: Furthermore, the Tokio runtime is old, so I have to use an older Hyper. +hyper = { version = "0.14.14" } + +# Crypto +bcrypt = "0.8" # password hashes +hmac = "0.10.0" # cookies +magic-crypt = "3.1.8" # encrypting sort IDs for the frontend so we don't leak info +md-5 = "0.9.1" # gravatar +sha2 = "0.9.5" # file hashes, inference text hashes, etc. + +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" + +# Text processing +pulldown-cmark = "0.8.0" + +[build-dependencies] +# NB: Only using diesel for migrations support +diesel = { version = "1.4.6", features = ["mysql"] } diff --git a/crates/service/image_generation_service/src/main.rs b/crates/service/image_generation_service/src/main.rs new file mode 100644 index 0000000000..6f60ced256 --- /dev/null +++ b/crates/service/image_generation_service/src/main.rs @@ -0,0 +1,246 @@ +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +//#![forbid(warnings)] +#![allow(unused_imports)] +#![allow(unused_mut)] +#![allow(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate magic_crypt; +#[macro_use] extern crate serde_derive; + +mod routes; +mod server_state; + +use actix_web::middleware::{DefaultHeaders, Logger}; +use actix_web::{App, HttpServer, web}; +use config::common_env::CommonEnv; +use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG}; +use container_common::anyhow_result::AnyhowResult; +use crate::routes::add_routes; +use crate::server_state::{EnvConfig, InMemoryCaches, ServerState}; +use http_server_common::cors::build_common_cors_config; +use limitation::Limiter; +use log::info; +use memory_caching::single_item_ttl_cache::SingleItemTtlCache; +use r2d2_redis::{r2d2, RedisConnectionManager}; +use sqlx::mysql::MySqlPoolOptions; +use std::sync::Arc; +use std::time::Duration; +use storage_buckets_common::bucket_client::BucketClient; +use tokio::runtime::Runtime; + +const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345"; + +#[actix_web::main] +async fn main() -> AnyhowResult<()> { + easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG)); + + // NB: Do not check this secrets-containing dotenv file into VCS. + // This file should only contain *development* secrets, never production. + let _ = dotenv::from_filename(".env-secrets").ok(); + + let common_env = CommonEnv::read_from_env()?; // TODO: Deprecate and remove + + info!("Obtaining hostname..."); + + let server_hostname = hostname::get() + .ok() + .and_then(|h| h.into_string().ok()) + .unwrap_or("image-generation-service".to_string()); + + info!("Hostname: {}", &server_hostname); + + info!("Connecting to database..."); + + let db_connection_string = + easyenv::get_env_string_or_default( + "MYSQL_URL", + DEFAULT_MYSQL_CONNECTION_STRING); + + let pool = MySqlPoolOptions::new() + .max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 5)?) + .connect(&db_connection_string) + .await?; + + // let firehose_publisher = FirehosePublisher { + // mysql_pool: pool.clone(), // NB: Pool is clone/sync/send-safe + // }; + // + // let badge_granter = BadgeGranter { + // mysql_pool: pool.clone(), // NB: Pool is clone/sync/send-safe + // firehose_publisher: firehose_publisher.clone(), // NB: Also safe + // }; + + let redis_manager = RedisConnectionManager::new( + common_env.redis_0_connection_string.clone())?; + + let redis_pool = r2d2::Pool::builder() + .build(redis_manager)?; + + info!("Setting up Redis rate limiters..."); + + //let logged_out_redis_rate_limiter = { + // let limiter_enabled = easyenv::get_env_bool_or_default("LIMITER_LOGGED_OUT_ENABLED", true); + // let limiter_max_requests = easyenv::get_env_num("LIMITER_LOGGED_OUT_MAX_REQUESTS", 3)?; + // let limiter_window_seconds = easyenv::get_env_num("LIMITER_LOGGED_OUT_WINDOW_SECONDS", 10)?; + // + // let limiter = Limiter::build(&common_env.redis_0_connection_string) + // .limit(limiter_max_requests) + // .period(Duration::from_secs(limiter_window_seconds)) + // .finish()?; + // + // RedisRateLimiter::new(limiter, "logged_out", limiter_enabled) + //}; + + //let logged_in_redis_rate_limiter = { + // let limiter_enabled = easyenv::get_env_bool_or_default("LIMITER_LOGGED_IN_ENABLED", true); + // let limiter_max_requests = easyenv::get_env_num("LIMITER_LOGGED_IN_MAX_REQUESTS", 3)?; + // let limiter_window_seconds = easyenv::get_env_num("LIMITER_LOGGED_IN_WINDOW_SECONDS", 10)?; + // + // let limiter = Limiter::build(&common_env.redis_0_connection_string) + // .limit(limiter_max_requests) + // .period(Duration::from_secs(limiter_window_seconds)) + // .finish()?; + // + // RedisRateLimiter::new(limiter, "logged_in", limiter_enabled) + //}; + + //let api_high_priority_redis_rate_limiter = { + // let limiter_enabled = easyenv::get_env_bool_or_default("LIMITER_API_HIGH_PRIORITY_ENABLED", true); + // let limiter_max_requests = easyenv::get_env_num("LIMITER_API_HIGH_PRIORITY_MAX_REQUESTS", 30)?; + // let limiter_window_seconds = easyenv::get_env_num("LIMITER_API_HIGH_PRIORITY_WINDOW_SECONDS", 30)?; + // + // let limiter = Limiter::build(&common_env.redis_0_connection_string) + // .limit(limiter_max_requests) + // .period(Duration::from_secs(limiter_window_seconds)) + // .finish()?; + // + // RedisRateLimiter::new(limiter, "api_high_priority", limiter_enabled) + //}; + + //let model_upload_rate_limiter = { + // let limiter_enabled = easyenv::get_env_bool_or_default("LIMITER_MODEL_UPLOAD_ENABLED", true); + // let limiter_max_requests = easyenv::get_env_num("LIMITER_MODEL_UPLOAD_MAX_REQUESTS", 3)?; + // let limiter_window_seconds = easyenv::get_env_num("LIMITER_MODEL_UPLOAD_WINDOW_SECONDS", 10)?; + // + // let limiter = Limiter::build(&common_env.redis_0_connection_string) + // .limit(limiter_max_requests) + // .period(Duration::from_secs(limiter_window_seconds)) + // .finish()?; + // + // RedisRateLimiter::new(limiter, "model_upload", limiter_enabled) + //}; + + info!("Reading env vars and setting up utils..."); + + let bind_address = easyenv::get_env_string_or_default("BIND_ADDRESS", DEFAULT_BIND_ADDRESS); + let num_workers = easyenv::get_env_num("NUM_WORKERS", 8)?; + let hmac_secret = easyenv::get_env_string_or_default("COOKIE_SECRET", "notsecret"); + let cookie_domain = easyenv::get_env_string_or_default("COOKIE_DOMAIN", ".vo.codes"); + let cookie_secure = easyenv::get_env_bool_or_default("COOKIE_SECURE", true); + let cookie_http_only = easyenv::get_env_bool_or_default("COOKIE_HTTP_ONLY", true); + let website_homepage_redirect = + easyenv::get_env_string_or_default("WEBSITE_HOMEPAGE_REDIRECT", "https://vo.codes/"); + + //let cookie_manager = CookieManager::new(&cookie_domain, &hmac_secret); + //let session_checker = SessionChecker::new(&cookie_manager); + + let access_key = easyenv::get_env_string_required("ACCESS_KEY")?; + let secret_key = easyenv::get_env_string_required("SECRET_KEY")?; + let region_name = easyenv::get_env_string_required("REGION_NAME")?; + + // NB: This secret really isn't too important. + // We can even rotate it without too much impact to users. + //let sort_key_crypto_secret = + // easyenv::get_env_string_or_default("SORT_KEY_SECRET", "webscale"); + //let sort_key_crypto = SortKeyCrypto::new(&sort_key_crypto_secret); + + // Background jobs. + + // let health_check_status = HealthCheckStatus::new(); + // let health_check_status2 = health_check_status.clone(); + // let ip_banlist = IpBanlistSet::new(); + // let ip_banlist2 = ip_banlist.clone(); + // let mysql_pool3 = pool.clone(); + // let mysql_pool4 = pool.clone(); + + let tokio_runtime = Runtime::new()?; + + // info!("Spawning DB health checker thread."); + // + // tokio_runtime.spawn(async { + // db_health_checker_thread(health_check_status2, mysql_pool3).await; + // }); + // + // info!("Spawning IP ban polling thread."); + // + // tokio_runtime.spawn(async { + // poll_ip_bans(ip_banlist2, mysql_pool4).await; + // }); + + let server_state = ServerState { + env_config: EnvConfig { + num_workers, + bind_address, + cookie_domain, + cookie_secure, + cookie_http_only, + website_homepage_redirect, + }, + hostname: server_hostname, + mysql_pool: pool, + redis_pool, + caches: InMemoryCaches { + }, + }; + + serve(server_state) + .await?; + Ok(()) +} + +pub async fn serve(server_state: ServerState) -> AnyhowResult<()> +{ + let bind_address = server_state.env_config.bind_address.clone(); + let num_workers = server_state.env_config.num_workers.clone(); + let hostname = server_state.hostname.clone(); + + let server_state_arc = web::Data::new(Arc::new(server_state)); + + info!("Starting HTTP service."); + + let log_format = "[%{HOSTNAME}e] %a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T"; + + HttpServer::new(move || { + // NB: Safe to clone due to internal arc + //let ip_banlist = server_state_arc.ip_banlist.clone(); + let build_sha = std::fs::read_to_string("/GIT_SHA").unwrap_or(String::from("unknown")); + + let app = App::new() + .app_data(server_state_arc.clone()) + .wrap(build_common_cors_config()) + .wrap(DefaultHeaders::new() + .header("X-Backend-Hostname", &hostname) + .header("X-Build-Sha", build_sha.trim())) + //.wrap(IpFilter::new(ip_banlist)) + .wrap(Logger::new(&log_format) + .exclude("/liveness") + .exclude("/readiness")); + + add_routes(app) + }) + .bind(bind_address)? + .workers(num_workers) + .run() + .await?; + + Ok(()) +} diff --git a/crates/service/image_generation_service/src/routes.rs b/crates/service/image_generation_service/src/routes.rs new file mode 100644 index 0000000000..cf0ac3de11 --- /dev/null +++ b/crates/service/image_generation_service/src/routes.rs @@ -0,0 +1,29 @@ +use actix_http::body::MessageBody; +use actix_service::ServiceFactory; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::error::Error; +use actix_web::{App, web, HttpResponse}; +use http_server_common::endpoints::default_route_404::default_route_404; +use http_server_common::endpoints::root_index::get_root_index; + +pub fn add_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app + //.service(web::resource("/_status") + // .route(web::get().to(get_health_check_handler)) + // .route(web::head().to(|| HttpResponse::Ok()))) + .service(web::resource("/") + .route(web::get().to(get_root_index)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .default_service( web::route().to(default_route_404)) +} diff --git a/crates/service/image_generation_service/src/server_state.rs b/crates/service/image_generation_service/src/server_state.rs new file mode 100644 index 0000000000..16a4276c91 --- /dev/null +++ b/crates/service/image_generation_service/src/server_state.rs @@ -0,0 +1,59 @@ +use r2d2_redis::{r2d2, RedisConnectionManager}; +use sqlx::MySqlPool; + +/// State that is injected into every endpoint. +pub struct ServerState { + pub env_config: EnvConfig, + + pub hostname: String, + + //pub health_check_status: HealthCheckStatus, + + pub mysql_pool: MySqlPool, + + pub redis_pool: r2d2::Pool, + + //pub redis_rate_limiters: RedisRateLimiters, + + //pub cookie_manager: CookieManager, + + //pub session_checker: SessionChecker, + + //pub sort_key_crypto: SortKeyCrypto, + + //pub ip_banlist: IpBanlistSet, + + pub caches: InMemoryCaches, +} + +#[derive(Clone)] +pub struct EnvConfig { + // Number of thread workers. + pub num_workers: usize, + pub bind_address: String, + pub cookie_domain: String, + pub cookie_secure: bool, + pub cookie_http_only: bool, + pub website_homepage_redirect: String, +} + +/// Different rate limiters for different users +//#[derive(Clone)] +//pub struct RedisRateLimiters { +// /// Logged out users have stricter limits +// pub logged_out: RedisRateLimiter, +// +// /// Logged in users have a little more leeway +// pub logged_in: RedisRateLimiter, +// +// /// API consumers have even higher priority +// /// (Temporary for VidVoice.ai; a long term solution builds an in-memory cache +// /// of these or finds a better rate limit library that allows on-demand rate +// /// constructions) +// pub api_high_priority: RedisRateLimiter, +//} + +/// In-memory caches +#[derive(Clone)] +pub struct InMemoryCaches { +} diff --git a/crates/service/storyteller_web/Cargo.toml b/crates/service/storyteller_web/Cargo.toml index 3bb8eb3e6b..aa788c4150 100644 --- a/crates/service/storyteller_web/Cargo.toml +++ b/crates/service/storyteller_web/Cargo.toml @@ -30,6 +30,9 @@ tts_common = { path = "../../lib/tts_common" } twitch_common = { path = "../../lib/twitch_common" } user_input_common = { path = "../../lib/user_input_common" } +# Internal Components +users_component = { path = "../../component_subsystem/users" } + # Vendored code newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs b/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs index 7d8169f506..72ca30758b 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::api_tokens::create_api_token::create_api_token_for_user; use database_queries::tokens::Tokens; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs b/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs index b9bb38519f..c18b3ac300 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::api_tokens::list_available_api_tokens_for_user::list_available_api_tokens_for_user; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use lexical_sort::natural_lexical_cmp; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs b/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs index 015ba9fa10..6f02d27889 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::model_categories::get_category_by_token::get_category_by_token; use database_queries::queries::tts::tts_models::get_tts_model::get_tts_model_by_token; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs b/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs index bb2f9f8d54..d502200c2f 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::tokens::Tokens; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs b/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs index 00fea85139..0447c38028 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::model_categories::get_category_by_token::get_category_by_token; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs index d825bf5658..245ed812b6 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; use database_queries::queries::generic_download::insert_generic_download_job::{Args, insert_generic_download_job}; use database_queries::tokens::Tokens; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs b/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs index ce2515f879..2e737facfb 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::request::parse_accept_language::parse_accept_language; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/mod.rs index ffb2ac00e1..be49be3664 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/mod.rs @@ -10,7 +10,6 @@ pub mod moderation; pub mod service; pub mod tts; pub mod twitch; -pub mod users; pub mod vocoders; pub mod voice_clone_requests; pub mod w2l; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs index b8f8021d0f..f7da41e637 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use log::{info, warn, log}; use regex::Regex; use sqlx::MySqlPool; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs index 5c19e5dee7..d682a16edb 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::model_categories::get_category_by_token::get_category_by_token; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs index e471a96184..f7062a8415 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::queries::model_categories::list_categories_query_builder::ListCategoriesQueryBuilder; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs index 3be5a80fe9..2dc640f0b9 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs @@ -1,7 +1,7 @@ use actix_http::Error; use actix_http::http::header; -use actix_web::cookie::Cookie; use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; @@ -9,17 +9,15 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::validations::ip_addresses::validate_moderator_provided_ip_address; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; -use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; +use std::fmt; use std::sync::Arc; +use user_input_common::validate_user_provided_ip_address::validate_user_provided_ip_address; #[derive(Deserialize)] pub struct AddIpBanRequest { @@ -28,7 +26,7 @@ pub struct AddIpBanRequest { maybe_target_user_token: Option, } -#[derive(Debug, Display)] +#[derive(Debug)] pub enum AddIpBanError { BadInput(String), ServerError, @@ -55,6 +53,13 @@ impl ResponseError for AddIpBanError { } } +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for AddIpBanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + pub async fn add_ip_ban_handler( http_request: HttpRequest, request: web::Json, @@ -85,7 +90,7 @@ pub async fn add_ip_ban_handler( let ip_address = request.ip_address.trim(); - if let Err(e) = validate_moderator_provided_ip_address(&ip_address) { + if let Err(e) = validate_user_provided_ip_address(&ip_address) { warn!("Bad ip address: {}", e); return Err(AddIpBanError::BadInput(e.to_string())); } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs index eba7577ba4..942e2efc35 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs @@ -11,8 +11,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs index ea1336c298..56bebec0e3 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs @@ -12,8 +12,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs index 9c7a00a993..237d26193a 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs @@ -12,8 +12,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs index 9683d99b9b..8d8724f1d6 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs @@ -9,7 +9,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error; use crate::server_state::ServerState; -use database_queries::queries::users::get_user_profile_by_username::get_user_profile_by_username; +use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username; use log::{info, warn, log}; use regex::Regex; use sqlx::error::DatabaseError; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs index 63bf3f01a7..523f5fc379 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs @@ -12,8 +12,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::users::user_roles::list_user_roles::{UserRoleForList, list_user_roles}; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs index 37216b3ec2..b89ad37446 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs @@ -12,8 +12,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs index acd6c34dfa..44a510bde5 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs @@ -11,9 +11,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; -use database_queries::queries::users::get_user_profile_by_username::get_user_profile_by_username; +use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username; use database_queries::queries::users::user_roles::list_user_roles::list_user_roles; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs index 2f4c5b45a0..1603eb5058 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs @@ -13,8 +13,6 @@ use crate::server_state::ServerState; use crate::util::delete_role_disambiguation::DeleteRole; use crate::util::delete_role_disambiguation::delete_role_disambiguation; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::tts::tts_results::delete_tts_result_various_scopes::delete_tts_inference_result_as_mod; use database_queries::queries::tts::tts_results::delete_tts_result_various_scopes::delete_tts_inference_result_as_user; use database_queries::queries::tts::tts_results::delete_tts_result_various_scopes::undelete_tts_inference_result_as_mod; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs index 5c400cb9ab..f1990f2c66 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs @@ -12,7 +12,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_models::edit_tts_model_details::{edit_tts_model_details_as_author, edit_tts_model_details_as_mod}; @@ -25,6 +24,7 @@ use std::fmt; use std::sync::Arc; use tts_common::text_pipelines::text_pipeline_type::TextPipelineType; use user_input_common::check_for_slurs::contains_slurs; +use user_input_common::markdown_to_html::markdown_to_html; const DEFAULT_IETF_LANGUAGE_TAG : &'static str = "en-US"; const DEFAULT_IETF_PRIMARY_LANGUAGE_SUBTAG : &'static str = "en"; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs index 15aff691b5..9059e0127f 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::tts::tts_results::query_tts_result::select_tts_result_by_token; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs index 81df3e4d32..e4b49c1181 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::tokens::Tokens; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs index 1273114a1e..d021cf3a92 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs @@ -9,8 +9,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::tokens::Tokens; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs index 3da2574f26..908a5b4432 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs @@ -10,10 +10,7 @@ use chrono::{DateTime, Utc}; use crate::AnyhowResult; use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::http_server::web_utils::session_checker::SessionRecord; use crate::server_state::ServerState; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::tts::tts_results::query_tts_result::TtsResultRecordForResponse; use database_queries::queries::tts::tts_results::query_tts_result::select_tts_result_by_token; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/list_user_tts_inference_results.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/users/list_user_tts_inference_results.rs rename to crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/list_user_tts_models.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/users/list_user_tts_models.rs rename to crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs index 0b0a6ad1f9..12b100e4a7 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs @@ -10,4 +10,6 @@ pub mod get_tts_model; pub mod get_tts_model_use_count; pub mod get_tts_result; pub mod get_tts_upload_model_job_status; -pub mod list_tts_models; \ No newline at end of file +pub mod list_tts_models; +pub mod list_user_tts_inference_results; +pub mod list_user_tts_models; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/logout.rs b/crates/service/storyteller_web/src/http_server/endpoints/users/logout.rs deleted file mode 100644 index f892574399..0000000000 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/logout.rs +++ /dev/null @@ -1,96 +0,0 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::cookie::Cookie; -use actix_web::HttpResponseBuilder; -use actix_web::error::ResponseError; -use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest, HttpMessage}; -use crate::AnyhowResult; -use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; -use crate::server_state::ServerState; -use derive_more::{Display, Error}; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; -use std::sync::Arc; - -#[derive(Serialize)] -pub struct LogoutSuccessResponse { - pub success: bool, -} - -#[derive(Debug, Display)] -pub enum LogoutError { - ServerError, -} - -impl ResponseError for LogoutError { - fn status_code(&self) -> StatusCode { - match *self { - LogoutError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - let error_reason = match self { - LogoutError::ServerError => "server error".to_string(), - }; - - to_simple_json_error(&error_reason, self.status_code()) - } -} - -pub async fn logout_handler( - http_request: HttpRequest, - server_state: web::Data>) -> Result -{ - let delete_cookie = match http_request.cookie("session") { - Some(cookie) => { - match server_state.cookie_manager.decode_session_token(&cookie) { - Err(e) => { - warn!("Session cookie decode error: {:?}", e); - }, - Ok(session_token) => { - let _r = delete_session(&session_token, &server_state.mysql_pool).await; - } - } - - cookie // delete this cookie - }, - None => { - server_state.cookie_manager.delete_cookie() - } - }; - - let response = LogoutSuccessResponse { - success: true, - }; - - let body = serde_json::to_string(&response) - .map_err(|e| LogoutError::ServerError)?; - - Ok(HttpResponse::Ok() - .del_cookie(&delete_cookie) - .content_type("application/json") - .body(body)) -} - -async fn delete_session(session_token: &str, mysql_pool: &MySqlPool) -> AnyhowResult<()> { - let query_result = sqlx::query!( - r#" -UPDATE user_sessions -SET deleted_at = CURRENT_TIMESTAMP() -WHERE - token = ? - AND deleted_at IS NULL - "#, - session_token.to_string(), - ) - .execute(mysql_pool) - .await; - - Ok(()) -} \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/users/mod.rs deleted file mode 100644 index 39f8c475ae..0000000000 --- a/crates/service/storyteller_web/src/http_server/endpoints/users/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod create_account; -pub mod edit_profile; -pub mod get_profile; -pub mod list_user_tts_inference_results; -pub mod list_user_tts_models; -pub mod list_user_vocoders; -pub mod list_user_w2l_inference_results; -pub mod list_user_w2l_templates; -pub mod login; -pub mod logout; -pub mod session_info; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs b/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs index ca2786f4d1..89847c66dd 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs @@ -1,4 +1,5 @@ pub mod delete_vocoder; pub mod edit_vocoder; pub mod get_vocoder; +pub mod list_user_vocoders; pub mod list_vocoders; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs index 4302111506..45a0a472a1 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs @@ -13,8 +13,6 @@ use crate::server_state::ServerState; use crate::util::delete_role_disambiguation::DeleteRole; use crate::util::delete_role_disambiguation::delete_role_disambiguation; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::delete_w2l_inference_result_as_mod; use database_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::delete_w2l_inference_result_as_user; use database_queries::queries::w2l::w2l_results::delete_w2l_result_various_scopes::undelete_w2l_inference_result_as_mod; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs index a23d27f0b0..5996958b35 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs @@ -18,8 +18,6 @@ use crate::server_state::ServerState; use crate::util::delete_role_disambiguation::DeleteRole; use crate::util::delete_role_disambiguation::delete_role_disambiguation; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use log::{info, warn, log}; use std::fmt; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs index 98c1301f5b..ed8bc427d7 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs index 20a4f93214..ced77255c0 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs @@ -1,28 +1,24 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::{Path, Json}; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use crate::util::email_to_gravatar::email_to_gravatar; -use crate::util::markdown_to_html::markdown_to_html; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token; -use log::{info, warn, log}; -use regex::Regex; +use log::{info, warn}; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; use user_input_common::check_for_slurs::contains_slurs; +use user_input_common::markdown_to_html::markdown_to_html; /// For the URL PathInfo #[derive(Deserialize)] diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs index 4ec1a9413b..70f5a999fa 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs @@ -9,8 +9,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::column_types::record_visibility::RecordVisibility; use log::{info, warn, log}; use r2d2_redis::redis::Commands; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs index 0087eafedf..1ca573861e 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs @@ -9,8 +9,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::tokens::Tokens; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs index ba3f883c4f..eb89444518 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs @@ -10,8 +10,6 @@ use chrono::{DateTime, Utc}; use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::w2l::w2l_templates::get_w2l_template::W2lTemplateRecordForResponse; use database_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token; use log::{info, warn, log}; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/list_user_w2l_inference_results.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/users/list_user_w2l_inference_results.rs rename to crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/users/list_user_w2l_templates.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/users/list_user_w2l_templates.rs rename to crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs index 0f55ac2244..f983874dda 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs @@ -10,5 +10,7 @@ pub mod get_w2l_result; pub mod get_w2l_template; pub mod get_w2l_template_use_count; pub mod get_w2l_upload_template_job_status; +pub mod list_user_w2l_inference_results; +pub mod list_user_w2l_templates; pub mod list_w2l_templates; pub mod set_w2l_template_mod_approval; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs b/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs index b6aab394a4..9d5d120bfd 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs @@ -10,8 +10,6 @@ use crate::http_server::web_utils::ip_address::get_request_ip; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; -use crate::validations::passwords::validate_passwords; -use crate::validations::username::validate_username; use database_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token; use log::{info, warn, log}; use regex::Regex; diff --git a/crates/service/storyteller_web/src/http_server/web_utils/mod.rs b/crates/service/storyteller_web/src/http_server/web_utils/mod.rs index 8037e1b8c8..c5d99398e3 100644 --- a/crates/service/storyteller_web/src/http_server/web_utils/mod.rs +++ b/crates/service/storyteller_web/src/http_server/web_utils/mod.rs @@ -1,9 +1,7 @@ -pub mod cookie_manager; pub mod get_host_header; pub mod ip_address; pub mod read_multipart_field_bytes; pub mod redis_rate_limiter; pub mod response_error_helpers; pub mod response_success_helpers; -pub mod serialize_as_json_error; -pub mod session_checker; \ No newline at end of file +pub mod serialize_as_json_error; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/web_utils/session_checker.rs b/crates/service/storyteller_web/src/http_server/web_utils/session_checker.rs deleted file mode 100644 index aa4e2aa247..0000000000 --- a/crates/service/storyteller_web/src/http_server/web_utils/session_checker.rs +++ /dev/null @@ -1,324 +0,0 @@ -// NB: Incrementally getting rid of build warnings... -#![forbid(unused_imports)] -#![forbid(unused_mut)] -#![forbid(unused_variables)] - -use actix_web::HttpRequest; -use anyhow::anyhow; -use crate::AnyhowResult; -use crate::http_server::web_utils::cookie_manager::CookieManager; -use database_queries::column_types::record_visibility::RecordVisibility; -use database_queries::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool}; -use log::warn; -use sqlx::error::Error::RowNotFound; -use sqlx::pool::PoolConnection; -use sqlx::{MySqlPool, MySql}; - -#[derive(Clone)] -pub struct SessionChecker { - cookie_manager: CookieManager, -} - -pub struct SessionRecord { - pub session_token: String, - pub user_token: String, -} - -pub struct SessionUserRecord { - pub user_token: String, - pub username: String, - pub display_name: String, - - pub email_address: String, - pub email_confirmed: bool, - pub email_gravatar_hash: String, - - // ===== PREFERENCES ===== // - - pub disable_gravatar: bool, - pub auto_play_audio_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, - pub auto_play_video_preference: Option, - - // ===== ROLE ===== // - - pub user_role_slug: String, - pub is_banned: bool, - - // ===== PERMISSIONS FLAGS ===== // - - // Usage - 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 - 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 - 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, -} - -pub struct SessionUserRawDbRecord { - pub user_token: String, - pub username: String, - pub display_name: String, - - pub email_address: String, - pub email_confirmed: i8, - pub email_gravatar_hash: String, - - pub disable_gravatar: i8, - pub auto_play_audio_preference: Option, - pub auto_play_video_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, - - pub user_role_slug: String, - pub is_banned: i8, - - // NB: These are `Option` due to the JOIN not being compile-time assured. - // Usage - pub can_use_tts: Option, - pub can_use_w2l: Option, - pub can_delete_own_tts_results: Option, - pub can_delete_own_w2l_results: Option, - pub can_delete_own_account: Option, - - // Contribution - pub can_upload_tts_models: Option, - pub can_upload_w2l_templates: Option, - pub can_delete_own_tts_models: Option, - pub can_delete_own_w2l_templates: Option, - - // Moderation - pub can_approve_w2l_templates: Option, - pub can_edit_other_users_profiles: Option, - pub can_edit_other_users_tts_models: Option, - pub can_edit_other_users_w2l_templates: Option, - pub can_delete_other_users_tts_models: Option, - pub can_delete_other_users_tts_results: Option, - pub can_delete_other_users_w2l_templates: Option, - pub can_delete_other_users_w2l_results: Option, - pub can_ban_users: Option, - pub can_delete_users: Option, -} - -impl SessionChecker { - - pub fn new(cookie_manager: &CookieManager) -> Self { - Self { - cookie_manager: cookie_manager.clone(), - } - } - - pub async fn maybe_get_session(&self, request: &HttpRequest, pool: &MySqlPool) - -> AnyhowResult> - { - let session_token = match self.cookie_manager.decode_session_token_from_request(request)? { - None => return Ok(None), - Some(session_token) => session_token, - }; - - // NB: Lookup failure is Err(RowNotFound). - let maybe_session_record = sqlx::query_as!( - SessionRecord, - r#" -SELECT - token as session_token, - user_token -FROM user_sessions -WHERE token = ? -AND deleted_at IS NULL - "#, - session_token.to_string(), - ) - .fetch_one(pool) - .await; // TODO: This will return error if it doesn't exist - - match maybe_session_record { - Ok(session_record) => { - Ok(Some(session_record)) - }, - Err(err) => { - match err { - RowNotFound => { - warn!("Valid cookie; invalid session: {}", session_token); - Ok(None) - }, - _ => { - warn!("Session query error: {:?}", err); - Err(anyhow!("session query error: {:?}", err)) - } - } - } - } - } - - #[deprecated = "Use the PoolConnection method instead of the MySqlPool one."] - pub async fn maybe_get_user_session( - &self, - request: &HttpRequest, - pool: &MySqlPool, - ) -> AnyhowResult> - { - let mut connection = pool.acquire().await?; - self.maybe_get_user_session_from_connection(request, &mut connection).await - } - - pub async fn maybe_get_user_session_from_connection( - &self, - request: &HttpRequest, - mysql_connection: &mut PoolConnection, - ) -> AnyhowResult> - { - - let session_token = match self.cookie_manager.decode_session_token_from_request(request)? { - None => return Ok(None), - Some(session_token) => session_token, - }; - - // NB: Lookup failure is Err(RowNotFound). - let maybe_user_record = sqlx::query_as!( - SessionUserRawDbRecord, - r#" -SELECT - users.token as user_token, - users.username, - users.display_name, - - users.email_address, - users.email_confirmed, - users.email_gravatar_hash, - - users.disable_gravatar, - users.auto_play_audio_preference, - users.auto_play_video_preference, - users.preferred_tts_result_visibility as `preferred_tts_result_visibility: database_queries::column_types::record_visibility::RecordVisibility`, - users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: database_queries::column_types::record_visibility::RecordVisibility`, - - users.user_role_slug, - users.is_banned, - - user_roles.can_use_tts, - user_roles.can_use_w2l, - user_roles.can_delete_own_tts_results, - user_roles.can_delete_own_w2l_results, - user_roles.can_delete_own_account, - - user_roles.can_upload_tts_models, - user_roles.can_upload_w2l_templates, - user_roles.can_delete_own_tts_models, - user_roles.can_delete_own_w2l_templates, - - user_roles.can_approve_w2l_templates, - user_roles.can_edit_other_users_profiles, - user_roles.can_edit_other_users_tts_models, - user_roles.can_edit_other_users_w2l_templates, - user_roles.can_delete_other_users_tts_models, - user_roles.can_delete_other_users_tts_results, - user_roles.can_delete_other_users_w2l_templates, - user_roles.can_delete_other_users_w2l_results, - user_roles.can_ban_users, - user_roles.can_delete_users - -FROM users -LEFT OUTER JOIN user_sessions - ON users.token = user_sessions.user_token -LEFT OUTER JOIN user_roles - ON users.user_role_slug = user_roles.slug -WHERE user_sessions.token = ? - AND user_sessions.deleted_at IS NULL - AND users.user_deleted_at IS NULL - AND users.mod_deleted_at IS NULL - "#, - session_token.to_string(), - ) - .fetch_one(mysql_connection) - .await; // TODO: This will return error if it doesn't exist - - match maybe_user_record { - Ok(raw_user_record) => { - let result_user_record = SessionUserRecord { - user_token: raw_user_record.user_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, - // Preference - disable_gravatar: i8_to_bool(raw_user_record.disable_gravatar), - auto_play_audio_preference: nullable_i8_to_optional_bool(raw_user_record.auto_play_audio_preference), - auto_play_video_preference: nullable_i8_to_optional_bool(raw_user_record.auto_play_video_preference), - user_role_slug: raw_user_record.user_role_slug, - preferred_tts_result_visibility: raw_user_record.preferred_tts_result_visibility, - preferred_w2l_result_visibility: raw_user_record.preferred_w2l_result_visibility, - - is_banned: i8_to_bool(raw_user_record.is_banned), - - // Usage - can_use_tts: convert_optional_db_bool_default_false(raw_user_record.can_use_tts), - can_use_w2l: convert_optional_db_bool_default_false(raw_user_record.can_use_w2l), - can_delete_own_tts_results: convert_optional_db_bool_default_false(raw_user_record.can_delete_own_tts_results), - can_delete_own_w2l_results: convert_optional_db_bool_default_false(raw_user_record.can_delete_own_w2l_results), - can_delete_own_account: convert_optional_db_bool_default_false(raw_user_record.can_delete_own_account), - // Contribution - can_upload_tts_models: convert_optional_db_bool_default_false(raw_user_record.can_upload_tts_models), - can_upload_w2l_templates: convert_optional_db_bool_default_false(raw_user_record.can_upload_w2l_templates), - can_delete_own_tts_models: convert_optional_db_bool_default_false(raw_user_record.can_delete_own_tts_models), - can_delete_own_w2l_templates: convert_optional_db_bool_default_false(raw_user_record.can_delete_own_w2l_templates), - // Moderation - can_approve_w2l_templates: convert_optional_db_bool_default_false(raw_user_record.can_approve_w2l_templates), - can_edit_other_users_profiles: convert_optional_db_bool_default_false(raw_user_record.can_edit_other_users_profiles), - can_edit_other_users_tts_models: convert_optional_db_bool_default_false(raw_user_record.can_edit_other_users_tts_models), - can_edit_other_users_w2l_templates: convert_optional_db_bool_default_false(raw_user_record.can_edit_other_users_w2l_templates), - can_delete_other_users_tts_models: convert_optional_db_bool_default_false(raw_user_record.can_delete_other_users_tts_models), - can_delete_other_users_tts_results: convert_optional_db_bool_default_false(raw_user_record.can_delete_other_users_tts_results), - can_delete_other_users_w2l_templates: convert_optional_db_bool_default_false(raw_user_record.can_delete_other_users_w2l_templates), - can_delete_other_users_w2l_results: convert_optional_db_bool_default_false(raw_user_record.can_delete_other_users_w2l_results), - can_ban_users: convert_optional_db_bool_default_false(raw_user_record.can_ban_users), - can_delete_users: convert_optional_db_bool_default_false(raw_user_record.can_delete_users) , - }; - - Ok(Some(result_user_record)) - }, - Err(err) => { - match err { - RowNotFound => { - warn!("Valid cookie; invalid session: {}", session_token); - Ok(None) - }, - _ => { - warn!("Session query error: {:?}", err); - Err(anyhow!("session query error: {:?}", err)) - } - } - } - } - } -} - -fn convert_optional_db_bool_default_false(value: Option) -> bool { - match value { - None => false, - Some(v) => match v { - 0 => false, - _ => true, - } - } -} diff --git a/crates/service/storyteller_web/src/main.rs b/crates/service/storyteller_web/src/main.rs index be16344dad..788d0fda14 100644 --- a/crates/service/storyteller_web/src/main.rs +++ b/crates/service/storyteller_web/src/main.rs @@ -77,16 +77,6 @@ use crate::http_server::endpoints::tts::get_tts_model_use_count::get_tts_model_u use crate::http_server::endpoints::tts::get_tts_result::get_tts_inference_result_handler; use crate::http_server::endpoints::tts::get_tts_upload_model_job_status::get_tts_upload_model_job_status_handler; use crate::http_server::endpoints::tts::list_tts_models::list_tts_models_handler; -use crate::http_server::endpoints::users::create_account::create_account_handler; -use crate::http_server::endpoints::users::edit_profile::edit_profile_handler; -use crate::http_server::endpoints::users::get_profile::get_profile_handler; -use crate::http_server::endpoints::users::list_user_tts_inference_results::list_user_tts_inference_results_handler; -use crate::http_server::endpoints::users::list_user_tts_models::list_user_tts_models_handler; -use crate::http_server::endpoints::users::list_user_w2l_inference_results::list_user_w2l_inference_results_handler; -use crate::http_server::endpoints::users::list_user_w2l_templates::list_user_w2l_templates_handler; -use crate::http_server::endpoints::users::login::login_handler; -use crate::http_server::endpoints::users::logout::logout_handler; -use crate::http_server::endpoints::users::session_info::session_info_handler; use crate::http_server::endpoints::w2l::delete_w2l_result::delete_w2l_inference_result_handler; use crate::http_server::endpoints::w2l::delete_w2l_template::delete_w2l_template_handler; use crate::http_server::endpoints::w2l::edit_w2l_result::edit_w2l_inference_result_handler; @@ -102,9 +92,7 @@ use crate::http_server::endpoints::w2l::get_w2l_upload_template_job_status::get_ use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_handler; use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; use crate::http_server::middleware::ip_filter_middleware::IpFilter; -use crate::http_server::web_utils::cookie_manager::CookieManager; use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter; -use crate::http_server::web_utils::session_checker::SessionChecker; use crate::routes::add_routes; use crate::server_state::{ServerState, EnvConfig, TwitchOauthSecrets, TwitchOauth, RedisRateLimiters, InMemoryCaches}; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; @@ -129,6 +117,8 @@ use std::time::Duration; use storage_buckets_common::bucket_client::BucketClient; use tokio::runtime::Runtime; use twitch_common::twitch_secrets::TwitchSecrets; +use users_component::utils::session_checker::SessionChecker; +use users_component::utils::session_cookie_manager::SessionCookieManager; // TODO TODO TODO TODO // TODO TODO TODO TODO @@ -275,7 +265,7 @@ async fn main() -> AnyhowResult<()> { let website_homepage_redirect = easyenv::get_env_string_or_default("WEBSITE_HOMEPAGE_REDIRECT", "https://vo.codes/"); - let cookie_manager = CookieManager::new(&cookie_domain, &hmac_secret); + let cookie_manager = SessionCookieManager::new(&cookie_domain, &hmac_secret); let session_checker = SessionChecker::new(&cookie_manager); let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?; @@ -435,7 +425,12 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()> let ip_banlist = server_state_arc.ip_banlist.clone(); let build_sha = std::fs::read_to_string("/GIT_SHA").unwrap_or(String::from("unknown")); + // NB: app_data being clone()'d below should all be safe (dependencies included) let app = App::new() + .app_data(web::Data::new(server_state_arc.firehose_publisher.clone())) + .app_data(web::Data::new(server_state_arc.mysql_pool.clone())) + .app_data(web::Data::new(server_state_arc.session_checker.clone())) + .app_data(web::Data::new(server_state_arc.cookie_manager.clone())) .app_data(server_state_arc.clone()) .wrap(build_common_cors_config()) .wrap(DefaultHeaders::new() diff --git a/crates/service/storyteller_web/src/routes.rs b/crates/service/storyteller_web/src/routes.rs index b3f12e7e6b..bf2a2457b2 100644 --- a/crates/service/storyteller_web/src/routes.rs +++ b/crates/service/storyteller_web/src/routes.rs @@ -57,6 +57,8 @@ use crate::http_server::endpoints::tts::get_tts_model_use_count::get_tts_model_u use crate::http_server::endpoints::tts::get_tts_result::get_tts_inference_result_handler; use crate::http_server::endpoints::tts::get_tts_upload_model_job_status::get_tts_upload_model_job_status_handler; use crate::http_server::endpoints::tts::list_tts_models::list_tts_models_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::twitch::event_rules::create_event_rule::create_twitch_event_rule_handler; use crate::http_server::endpoints::twitch::event_rules::delete_event_rule::delete_twitch_event_rule_handler; use crate::http_server::endpoints::twitch::event_rules::edit_event_rule::edit_twitch_event_rule_handler; @@ -67,16 +69,6 @@ use crate::http_server::endpoints::twitch::oauth::check_oauth_status::check_oaut use crate::http_server::endpoints::twitch::oauth::oauth_begin_json::oauth_begin_enroll_json; use crate::http_server::endpoints::twitch::oauth::oauth_begin_redirect::oauth_begin_enroll_redirect; use crate::http_server::endpoints::twitch::oauth::oauth_end::oauth_end_enroll_from_redirect; -use crate::http_server::endpoints::users::create_account::create_account_handler; -use crate::http_server::endpoints::users::edit_profile::edit_profile_handler; -use crate::http_server::endpoints::users::get_profile::get_profile_handler; -use crate::http_server::endpoints::users::list_user_tts_inference_results::list_user_tts_inference_results_handler; -use crate::http_server::endpoints::users::list_user_tts_models::list_user_tts_models_handler; -use crate::http_server::endpoints::users::list_user_w2l_inference_results::list_user_w2l_inference_results_handler; -use crate::http_server::endpoints::users::list_user_w2l_templates::list_user_w2l_templates_handler; -use crate::http_server::endpoints::users::login::login_handler; -use crate::http_server::endpoints::users::logout::logout_handler; -use crate::http_server::endpoints::users::session_info::session_info_handler; use crate::http_server::endpoints::vocoders::get_vocoder::get_vocoder_handler; use crate::http_server::endpoints::vocoders::list_vocoders::list_vocoders_handler; use crate::http_server::endpoints::voice_clone_requests::check_if_voice_clone_request_submitted::check_if_voice_clone_request_submitted_handler; @@ -93,8 +85,13 @@ use crate::http_server::endpoints::w2l::get_w2l_result::get_w2l_inference_result use crate::http_server::endpoints::w2l::get_w2l_template::get_w2l_template_handler; use crate::http_server::endpoints::w2l::get_w2l_template_use_count::get_w2l_template_use_count_handler; use crate::http_server::endpoints::w2l::get_w2l_upload_template_job_status::get_w2l_upload_template_job_status_handler; +use crate::http_server::endpoints::w2l::list_user_w2l_inference_results::list_user_w2l_inference_results_handler; +use crate::http_server::endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler; use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_handler; use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; +use users_component::default_routes::add_suggested_api_v1_account_creation_and_session_routes; +use users_component::endpoints::edit_profile_handler::edit_profile_handler; +use users_component::endpoints::get_profile_handler::get_profile_handler; pub fn add_routes (app: App) -> App where @@ -120,33 +117,15 @@ pub fn add_routes (app: App) -> App app = add_investor_demo_routes(app); /* /demo_mode */ // TODO: DEFINITELY TEMPORARY app = add_flag_routes(app); /* /flag */ + // From components + app = add_suggested_api_v1_account_creation_and_session_routes(app); // /create_account, /session, /login, /logout + // ==================== SERVICE ==================== app.service( web::resource("/_status") .route(web::get().to(get_health_check_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - // ==================== ACCOUNT CREATION / SESSION MANAGEMENT ==================== - .service( - web::resource("/create_account") - .route(web::post().to(create_account_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - .service( - web::resource("/login") - .route(web::post().to(login_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - .service( - web::resource("/logout") - .route(web::post().to(logout_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - .service( - web::resource("/session") - .route(web::get().to(session_info_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) // ==================== MISC ==================== .service( web::resource("/events") diff --git a/crates/service/storyteller_web/src/server_state.rs b/crates/service/storyteller_web/src/server_state.rs index 30130f5afd..f529870c6a 100644 --- a/crates/service/storyteller_web/src/server_state.rs +++ b/crates/service/storyteller_web/src/server_state.rs @@ -1,20 +1,20 @@ use crate::StaticApiTokenSet; use crate::http_server::endpoints::categories::list_tts_categories::DisplayCategory; use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse; -use crate::http_server::web_utils::cookie_manager::CookieManager; use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter; -use crate::http_server::web_utils::session_checker::SessionChecker; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; use crate::threads::ip_banlist_set::IpBanlistSet; use crate::util::encrypted_sort_id::SortKeyCrypto; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; +use database_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult; +use database_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList; use memory_caching::single_item_ttl_cache::SingleItemTtlCache; use r2d2_redis::{r2d2, RedisConnectionManager}; use sqlx::MySqlPool; -use database_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult; -use database_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList; use storage_buckets_common::bucket_client::BucketClient; +use users_component::utils::session_checker::SessionChecker; +use users_component::utils::session_cookie_manager::SessionCookieManager; /// State that is injected into every endpoint. pub struct ServerState { @@ -31,7 +31,7 @@ pub struct ServerState { pub redis_rate_limiters: RedisRateLimiters, - pub cookie_manager: CookieManager, + pub cookie_manager: SessionCookieManager, pub session_checker: SessionChecker, diff --git a/crates/service/storyteller_web/src/util/email_to_gravatar.rs b/crates/service/storyteller_web/src/util/email_to_gravatar.rs deleted file mode 100644 index 26f4f0fea8..0000000000 --- a/crates/service/storyteller_web/src/util/email_to_gravatar.rs +++ /dev/null @@ -1,12 +0,0 @@ -use md5::{Md5, Digest}; - -pub fn email_to_gravatar(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 -} diff --git a/crates/service/storyteller_web/src/util/mod.rs b/crates/service/storyteller_web/src/util/mod.rs index 14982ea904..342219d99d 100644 --- a/crates/service/storyteller_web/src/util/mod.rs +++ b/crates/service/storyteller_web/src/util/mod.rs @@ -1,4 +1,2 @@ pub mod delete_role_disambiguation; -pub mod email_to_gravatar; -pub mod encrypted_sort_id; -pub mod markdown_to_html; \ No newline at end of file +pub mod encrypted_sort_id; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/validations/ip_addresses.rs b/crates/service/storyteller_web/src/validations/ip_addresses.rs deleted file mode 100644 index 20a22d4e45..0000000000 --- a/crates/service/storyteller_web/src/validations/ip_addresses.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::AnyhowResult; -use regex::Regex; - -/// Do not use this for IP addresses from requests. -/// Only for moderator-input IP addresses -pub fn validate_moderator_provided_ip_address(ip_address: &str) -> Result<(), String> { - lazy_static! { - static ref IP_ADDRESS_REGEX: Regex = { - // TODO: This doesn't require valid IP addresses. Just a number/dot format. - Regex::new(r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$").expect("should be valid regex") - }; - } - - if !IP_ADDRESS_REGEX.is_match(ip_address) { - return Err("ip address is invalid".to_string()); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::validations::ip_addresses::validate_moderator_provided_ip_address; - - #[test] - fn valid_cases() { - assert!(validate_moderator_provided_ip_address("0.0.0.0").is_ok()); - assert!(validate_moderator_provided_ip_address("1.1.1.1").is_ok()); - assert!(validate_moderator_provided_ip_address("1.2.3.4").is_ok()); - assert!(validate_moderator_provided_ip_address("111.222.3.0").is_ok()); - assert!(validate_moderator_provided_ip_address("127.0.0.1").is_ok()); - assert!(validate_moderator_provided_ip_address("255.255.255.255").is_ok()); - } - - #[test] - fn invalid_cases() { - assert!(validate_moderator_provided_ip_address(" 255.255.255.255 ").is_err()); - assert!(validate_moderator_provided_ip_address("").is_err()); - assert!(validate_moderator_provided_ip_address(".1.1.1.1").is_err()); - assert!(validate_moderator_provided_ip_address("1.1.1").is_err()); - assert!(validate_moderator_provided_ip_address("127.0.0.1notvalid").is_err()); - assert!(validate_moderator_provided_ip_address("127.127.127.127.").is_err()); - } -} diff --git a/crates/service/storyteller_web/src/validations/mod.rs b/crates/service/storyteller_web/src/validations/mod.rs index 073fcfcec1..fac4c4e863 100644 --- a/crates/service/storyteller_web/src/validations/mod.rs +++ b/crates/service/storyteller_web/src/validations/mod.rs @@ -1,12 +1,2 @@ -pub mod cashapp_username; -pub mod discord_username; -pub mod github_username; -pub mod ip_addresses; pub mod model_uploads; -pub mod passwords; -pub mod twitch_username; -pub mod twitter_username; -pub mod username; -pub mod username_reservations; -pub mod validate_idempotency_token_format; -pub mod website_url; \ No newline at end of file +pub mod validate_idempotency_token_format; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/validations/twitch_username.rs b/crates/service/storyteller_web/src/validations/twitch_username.rs deleted file mode 100644 index e93509ffae..0000000000 --- a/crates/service/storyteller_web/src/validations/twitch_username.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::AnyhowResult; -use regex::Regex; - -pub fn validate_twitch_username(username: &str) -> Result<(), String> { - lazy_static! { - static ref TWITCH_USERNAME_REGEX: Regex = { - Regex::new(r"^[a-zA-Z0-9]{4,25}$").expect("should be valid regex") - }; - } - - if username.len() < 4 { - return Err("twitch username is too short".to_string()); - } - - if username.len() > 25 { - return Err("twitch username is too long".to_string()); - } - - if !TWITCH_USERNAME_REGEX.is_match(username) { - return Err("twitch username invalid".to_string()); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::validations::twitch_username::validate_twitch_username; - - #[test] - fn valid_cases() { - assert!(validate_twitch_username("echelon").is_ok()); - assert!(validate_twitch_username("1234").is_ok()); - assert!(validate_twitch_username("1234567890123456789012345").is_ok()); - } - - #[test] - fn invalid_cases() { - assert!(validate_twitch_username("").is_err()); - assert!(validate_twitch_username("@=[]@34!").is_err()); - assert!(validate_twitch_username("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err()); - } -} \ No newline at end of file diff --git a/db/reserved_usernames.txt b/db/reserved_usernames.txt index 10f6d1ff8e..3c86313eeb 100644 --- a/db/reserved_usernames.txt +++ b/db/reserved_usernames.txt @@ -7,8 +7,11 @@ # Stuff we care about fake_you +fakeu fakeyou hanashi +hyper_jail +hyperjail monogatari storied stories @@ -23,15 +26,26 @@ vocodes voxelcam # Types of models +diffusion first_order_model fom hifigan melgan +stable_diffusion +stablediffusion tacotron tt2 tts w2l +# Competitors +15_ai +15ai +fifteen_ai +fifteenai +uber_duck +uberduck + # Companies we'd love to have use our service # so we'll reserve them to be safe amazon diff --git a/db/reserved_usernames_including.txt b/db/reserved_usernames_including.txt index cadc30d175..61addebcb6 100644 --- a/db/reserved_usernames_including.txt +++ b/db/reserved_usernames_including.txt @@ -2,6 +2,7 @@ # Any of the substrings may not be used. # You should check string.remove('_') as well. fakeyou +hyperjail storytell storyteller test diff --git a/sqlx-data.json b/sqlx-data.json index 9aaf79bc4a..597774fdfc 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1700,6 +1700,16 @@ ] } }, + "1cc62e5cdc8de18969d7e8018d91c38794e5a7d23f117e5b0a4126845f300e60": { + "query": "\nUPDATE user_sessions\nSET deleted_at = CURRENT_TIMESTAMP()\nWHERE\n token = ?\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + } + }, "28cf5843d68d6cb0e67eb316ef9e7e5fb05a6be2433f86c081576b279167508d": { "query": "\nINSERT INTO tts_result_synthetic_ids\nSET\n user_token = ?,\n next_id = 1\nON DUPLICATE KEY UPDATE\n user_token = ?,\n next_id = next_id + 1\n ", "describe": { @@ -2216,6 +2226,16 @@ ] } }, + "34364f9bc851971f8291f5f9942dd23d10718ccb210a93a236d96d0591f65f35": { + "query": "\nINSERT INTO users (\n token,\n username,\n display_name,\n email_address,\n email_gravatar_hash,\n profile_markdown,\n profile_rendered_html,\n user_role_slug,\n password_hash,\n ip_address_creation,\n ip_address_last_login,\n ip_address_last_update\n)\nVALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + } + }, "345cbabbe69652c89ccd384ed7db44f70c53791488cf63721f9563291b45db3e": { "query": "\nINSERT INTO user_sessions (\n token,\n user_token,\n ip_address_creation,\n expires_at\n)\nVALUES ( ?, ?, ?, NOW() + interval 1 year )\n ", "describe": { @@ -2256,6 +2276,16 @@ "nullable": [] } }, + "3a2dc0a7e41dcbdf21f91d1a1e54e813f74b0bac7aea052302089cae195eeb53": { + "query": "\nUPDATE users\nSET\n profile_markdown = ?,\n profile_rendered_html = ?,\n discord_username = ?,\n twitter_username = ?,\n twitch_username = ?,\n github_username = ?,\n cashapp_username = ?,\n website_url = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + } + }, "40a3301e9a3201e74d49b6b339f67291e2ae22d315188401708609ac2d28a68c": { "query": "\nUPDATE tts_results\nSET\n mod_deleted_at = NULL,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ", "describe": { @@ -4232,6 +4262,83 @@ "nullable": [] } }, + "7490dcb8de10b1eda2c857e53ebc5c0465cdb589d3df4c5549859f7919acac77": { + "query": "\nSELECT token, username, email_address, password_hash, is_banned\nFROM users\nWHERE email_address = ?\nLIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "username", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 2, + "name": "email_address", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "password_hash", + "type_info": { + "type": "String", + "flags": { + "bits": 4225 + }, + "char_set": 63, + "max_size": 60 + } + }, + { + "ordinal": 4, + "name": "is_banned", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 1 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, "764c8dc4fc9b021b58a618b5f9ee6e0c9c08ac6d58b1f4c05455e951a0d01708": { "query": "\nSELECT\n id,\n token AS inference_job_token,\n uuid_idempotency_token,\n\n model_token,\n raw_inference_text,\n\n creator_ip_address,\n maybe_creator_user_token,\n creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`,\n\n is_from_api,\n is_for_twitch,\n is_debug_request,\n\n status as `status: crate::column_types::job_status::JobStatus`,\n priority_level,\n attempt_count,\n failure_reason,\n\n created_at,\n updated_at,\n retry_at\nFROM tts_inference_jobs\nWHERE\n is_debug_request = ?\n AND\n (\n status IN (\"pending\", \"attempt_failed\")\n )\n AND\n (\n retry_at IS NULL\n OR\n retry_at < CURRENT_TIMESTAMP\n )\n ORDER BY priority_level DESC, id ASC\n LIMIT ?\n ", "describe": { @@ -9603,6 +9710,434 @@ ] } }, + "c3f3792382c648d554c406e03d5109bc0257a619627aec96100a85b3ee503493": { + "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.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: crate::column_types::record_visibility::RecordVisibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: crate::column_types::record_visibility::RecordVisibility`,\n\n users.user_role_slug,\n users.is_banned,\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": [ + { + "ordinal": 0, + "name": "user_token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "username", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4225 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "email_address", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 4, + "name": "email_confirmed", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 5, + "name": "email_gravatar_hash", + "type_info": { + "type": "String", + "flags": { + "bits": 4225 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "disable_gravatar", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 7, + "name": "auto_play_audio_preference", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 8, + "name": "auto_play_video_preference", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 9, + "name": "preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility", + "type_info": { + "type": "String", + "flags": { + "bits": 385 + }, + "char_set": 224, + "max_size": 28 + } + }, + { + "ordinal": 10, + "name": "preferred_w2l_result_visibility: crate::column_types::record_visibility::RecordVisibility", + "type_info": { + "type": "String", + "flags": { + "bits": 385 + }, + "char_set": 224, + "max_size": 28 + } + }, + { + "ordinal": 11, + "name": "user_role_slug", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4233 + }, + "char_set": 224, + "max_size": 64 + } + }, + { + "ordinal": 12, + "name": "is_banned", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 13, + "name": "can_use_tts", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 14, + "name": "can_use_w2l", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 15, + "name": "can_delete_own_tts_results", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 16, + "name": "can_delete_own_w2l_results", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 17, + "name": "can_delete_own_account", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 18, + "name": "can_upload_tts_models", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 19, + "name": "can_upload_w2l_templates", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 20, + "name": "can_delete_own_tts_models", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 21, + "name": "can_delete_own_w2l_templates", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 22, + "name": "can_approve_w2l_templates", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 23, + "name": "can_edit_other_users_profiles", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 24, + "name": "can_edit_other_users_tts_models", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 25, + "name": "can_edit_other_users_w2l_templates", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 26, + "name": "can_delete_other_users_tts_models", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 27, + "name": "can_delete_other_users_tts_results", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 28, + "name": "can_delete_other_users_w2l_templates", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 29, + "name": "can_delete_other_users_w2l_results", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 30, + "name": "can_ban_users", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 31, + "name": "can_delete_users", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 0 + }, + "char_set": 63, + "max_size": 1 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true + ] + } + }, "c4640a618e1d179fa95adce67a329818f8b1626ac88fa76dbb871b8788136191": { "query": "\nUPDATE w2l_templates\nSET\n maybe_mod_user_token = ?,\n mod_deleted_at = NULL\nWHERE\n token = ?\nLIMIT 1\n ", "describe": { @@ -10874,6 +11409,44 @@ ] } }, + "e73192a3e763a287d5737821443c5a81649f17e39978497b99c52e1e896a75f6": { + "query": "\nSELECT\n token as session_token,\n user_token\nFROM user_sessions\nWHERE token = ?\nAND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "session_token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "user_token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4233 + }, + "char_set": 224, + "max_size": 128 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + } + }, "e89d571f95a4fad69c5531eab5d5cae425d351ee8303ca25084ece84e5a02949": { "query": "\nSELECT\n vocoder.token as vocoder_token,\n vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`,\n vocoder.creator_user_token,\n users.username as creator_username,\n users.display_name as creator_display_name,\n users.email_gravatar_hash as creator_gravatar_hash,\n vocoder.title,\n vocoder.is_staff_recommended,\n vocoder.is_mod_disabled_from_public_use,\n vocoder.is_mod_disabled_from_author_use,\n vocoder.is_mod_author_editing_locked,\n vocoder.created_at,\n vocoder.updated_at,\n vocoder.user_deleted_at,\n vocoder.mod_deleted_at\nFROM vocoder_models as vocoder\nJOIN users\n ON users.token = vocoder.creator_user_token\nWHERE\n users.username = ?\n AND vocoder.is_mod_disabled_from_public_use IS FALSE\n AND vocoder.is_mod_disabled_from_author_use IS FALSE\n AND vocoder.user_deleted_at IS NULL\n AND vocoder.mod_deleted_at IS NULL\n ", "describe": { @@ -11356,6 +11929,16 @@ "nullable": [] } }, + "f75b713d166398ebea2bdbde4e416dc8eb2c48dba4aa9bd164bcf5bc77237ce1": { + "query": "\nUPDATE users\nSET\n profile_markdown = ?,\n profile_rendered_html = ?,\n preferred_tts_result_visibility = ?,\n preferred_w2l_result_visibility = ?,\n discord_username = ?,\n twitter_username = ?,\n twitch_username = ?,\n github_username = ?,\n cashapp_username = ?,\n website_url = ?,\n ip_address_last_update = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + } + }, "fb3b4c13ef5ef662e808c85fbdf28a98de14af5390c206e074aa8a4ac67a90fe": { "query": "\nINSERT INTO tts_results\nSET\n token = ?,\n\n model_token = ?,\n maybe_pretrained_vocoder_used = ?,\n raw_inference_text = ?,\n raw_inference_text_hash_sha2 = ?,\n normalized_inference_text = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_synthetic_id = ?,\n\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n public_bucket_wav_audio_path = ?,\n public_bucket_spectrogram_path = ?,\n\n file_size_bytes = ?,\n duration_millis = ?,\n\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n\n is_debug_request = ?\n ", "describe": { @@ -11391,6 +11974,83 @@ ] } }, + "feb0dae816a5701707a76e15011ea6cb262b10c663abf2cf1fb46bd9b1cebe09": { + "query": "\nSELECT token, username, email_address, password_hash, is_banned\nFROM users\nWHERE username = ?\nLIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "username", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 2, + "name": "email_address", + "type_info": { + "type": "VarString", + "flags": { + "bits": 4229 + }, + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "password_hash", + "type_info": { + "type": "String", + "flags": { + "bits": 4225 + }, + "char_set": 63, + "max_size": 60 + } + }, + { + "ordinal": 4, + "name": "is_banned", + "type_info": { + "type": "Tiny", + "flags": { + "bits": 1 + }, + "char_set": 63, + "max_size": 1 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, "ffa85f7c65101a43653c724479ee080e0a74577d2ae37863b039f3a452493d1e": { "query": "\nUPDATE w2l_results\nSET\n user_deleted_at = CURRENT_TIMESTAMP\nWHERE\n token = ?\nLIMIT 1\n ", "describe": { @@ -11421,16 +12081,6 @@ "nullable": [] } }, - "1cc62e5cdc8de18969d7e8018d91c38794e5a7d23f117e5b0a4126845f300e60": { - "query": "\nUPDATE user_sessions\nSET deleted_at = CURRENT_TIMESTAMP()\nWHERE\n token = ?\n AND deleted_at IS NULL\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - } - }, "1d0f302c0b7c4301000c024d6bbae4d3b7370ed7c944e4c557c64b0bd2f31c14": { "query": "\nSELECT public_count, all_count FROM (\n (SELECT count(*) as public_count \n FROM tts_models \n WHERE user_deleted_at IS NULL \n AND mod_deleted_at IS NULL) as pc,\n (SELECT count(*) as all_count \n FROM tts_models) as ac\n)\n ", "describe": { @@ -11499,16 +12149,6 @@ "nullable": [] } }, - "34364f9bc851971f8291f5f9942dd23d10718ccb210a93a236d96d0591f65f35": { - "query": "\nINSERT INTO users (\n token,\n username,\n display_name,\n email_address,\n email_gravatar_hash,\n profile_markdown,\n profile_rendered_html,\n user_role_slug,\n password_hash,\n ip_address_creation,\n ip_address_last_login,\n ip_address_last_update\n)\nVALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - } - }, "346c02b215cb35efaa35fe96d866a8c9056ac46e987b4e01f64222259dccdcbf": { "query": "\nUPDATE\n ip_address_bans\nSET\n mod_user_token = ?,\n deleted_at = NULL\nWHERE\n ip_address = ?\nLIMIT 1\n ", "describe": { @@ -11519,16 +12159,6 @@ "nullable": [] } }, - "3a2dc0a7e41dcbdf21f91d1a1e54e813f74b0bac7aea052302089cae195eeb53": { - "query": "\nUPDATE users\nSET\n profile_markdown = ?,\n profile_rendered_html = ?,\n discord_username = ?,\n twitter_username = ?,\n twitch_username = ?,\n github_username = ?,\n cashapp_username = ?,\n website_url = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - } - }, "5473d3857478d001688998056377fefe4bcc792477f303b41fa3cf64f79170f3": { "query": "\n INSERT INTO tts_category_assignments\n SET\n model_token = ?,\n category_token = ?,\n category_removal_user_token = ?,\n ip_address_creation = ?,\n ip_address_last_update = ?,\n deleted_at = CURRENT_TIMESTAMP\n ON DUPLICATE KEY UPDATE\n model_token = ?,\n category_token = ?,\n category_removal_user_token = ?,\n ip_address_last_update = ?,\n deleted_at = CURRENT_TIMESTAMP\n ", "describe": { @@ -11956,83 +12586,6 @@ "nullable": [] } }, - "7490dcb8de10b1eda2c857e53ebc5c0465cdb589d3df4c5549859f7919acac77": { - "query": "\nSELECT token, username, email_address, password_hash, is_banned\nFROM users\nWHERE email_address = ?\nLIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "username", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 2, - "name": "email_address", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 3, - "name": "password_hash", - "type_info": { - "type": "String", - "flags": { - "bits": 4225 - }, - "char_set": 63, - "max_size": 60 - } - }, - { - "ordinal": 4, - "name": "is_banned", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 1 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } - }, "7622292c05fe1b5832136762042780a12a02048166bb1b4492dfd627c48c8968": { "query": "\nSELECT\n token as template_token\nFROM w2l_templates\nWHERE\n token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\n ", "describe": { @@ -12680,44 +13233,6 @@ ] } }, - "e73192a3e763a287d5737821443c5a81649f17e39978497b99c52e1e896a75f6": { - "query": "\nSELECT\n token as session_token,\n user_token\nFROM user_sessions\nWHERE token = ?\nAND deleted_at IS NULL\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "session_token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "user_token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4233 - }, - "char_set": 224, - "max_size": 128 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false - ] - } - }, "eab69a8d679b2dcfb06a9ccc79851f6c693d0c372142ff0e7ded89c95e4bcf44": { "query": "\nUPDATE users\nSET\n is_banned = ?,\n maybe_mod_comments = ?,\n maybe_mod_user_token = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ", "describe": { @@ -12934,444 +13449,6 @@ ] } }, - "f70bd775718829cd10f508dfe09286020467cc34fb54daf0758dba2545b164f8": { - "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.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: database_queries::column_types::record_visibility::RecordVisibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: database_queries::column_types::record_visibility::RecordVisibility`,\n\n users.user_role_slug,\n users.is_banned,\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": [ - { - "ordinal": 0, - "name": "user_token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "username", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 2, - "name": "display_name", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4225 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 3, - "name": "email_address", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 4, - "name": "email_confirmed", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 5, - "name": "email_gravatar_hash", - "type_info": { - "type": "String", - "flags": { - "bits": 4225 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 6, - "name": "disable_gravatar", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 7, - "name": "auto_play_audio_preference", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 8, - "name": "auto_play_video_preference", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 9, - "name": "preferred_tts_result_visibility: database_queries::column_types::record_visibility::RecordVisibility", - "type_info": { - "type": "String", - "flags": { - "bits": 385 - }, - "char_set": 224, - "max_size": 28 - } - }, - { - "ordinal": 10, - "name": "preferred_w2l_result_visibility: database_queries::column_types::record_visibility::RecordVisibility", - "type_info": { - "type": "String", - "flags": { - "bits": 385 - }, - "char_set": 224, - "max_size": 28 - } - }, - { - "ordinal": 11, - "name": "user_role_slug", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4233 - }, - "char_set": 224, - "max_size": 64 - } - }, - { - "ordinal": 12, - "name": "is_banned", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 13, - "name": "can_use_tts", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 14, - "name": "can_use_w2l", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 15, - "name": "can_delete_own_tts_results", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 16, - "name": "can_delete_own_w2l_results", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 17, - "name": "can_delete_own_account", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 18, - "name": "can_upload_tts_models", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 19, - "name": "can_upload_w2l_templates", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 20, - "name": "can_delete_own_tts_models", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 21, - "name": "can_delete_own_w2l_templates", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 22, - "name": "can_approve_w2l_templates", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 23, - "name": "can_edit_other_users_profiles", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 24, - "name": "can_edit_other_users_tts_models", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 25, - "name": "can_edit_other_users_w2l_templates", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 26, - "name": "can_delete_other_users_tts_models", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 27, - "name": "can_delete_other_users_tts_results", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 28, - "name": "can_delete_other_users_w2l_templates", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 29, - "name": "can_delete_other_users_w2l_results", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 30, - "name": "can_ban_users", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - }, - { - "ordinal": 31, - "name": "can_delete_users", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 0 - }, - "char_set": 63, - "max_size": 1 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true - ] - } - }, - "f75b713d166398ebea2bdbde4e416dc8eb2c48dba4aa9bd164bcf5bc77237ce1": { - "query": "\nUPDATE users\nSET\n profile_markdown = ?,\n profile_rendered_html = ?,\n preferred_tts_result_visibility = ?,\n preferred_w2l_result_visibility = ?,\n discord_username = ?,\n twitter_username = ?,\n twitch_username = ?,\n github_username = ?,\n cashapp_username = ?,\n website_url = ?,\n ip_address_last_update = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - } - }, "fd31f7abe386147c07f862d668c9e56eb3d809c4f0a61b7debf9b2e3785a4c83": { "query": "\nINSERT INTO model_categories\nSET\n token = ?,\n uuid_idempotency_token = ?,\n model_type = ?,\n name = ?,\n\n creator_user_token = ?,\n creator_ip_address_creation = ?,\n creator_ip_address_last_update = ?,\n\n is_mod_approved = ?,\n maybe_mod_user_token = ?,\n can_directly_have_models = ?,\n can_have_subcategories = ?,\n can_only_mods_apply = ?\n ", "describe": { @@ -13381,82 +13458,5 @@ }, "nullable": [] } - }, - "feb0dae816a5701707a76e15011ea6cb262b10c663abf2cf1fb46bd9b1cebe09": { - "query": "\nSELECT token, username, email_address, password_hash, is_banned\nFROM users\nWHERE username = ?\nLIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "token", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "username", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 2, - "name": "email_address", - "type_info": { - "type": "VarString", - "flags": { - "bits": 4229 - }, - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 3, - "name": "password_hash", - "type_info": { - "type": "String", - "flags": { - "bits": 4225 - }, - "char_set": 63, - "max_size": 60 - } - }, - { - "ordinal": 4, - "name": "is_banned", - "type_info": { - "type": "Tiny", - "flags": { - "bits": 1 - }, - "char_set": 63, - "max_size": 1 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } } }