mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-11 20:32:12 +00:00
Merge pull request #9 from storytold/echelon-billing-and-images
Pull user system into a reusable component library
This commit is contained in:
120 files changed
+3216
-1775
No files matched your search
Generated
+1
-4
@@ -1,12 +1,9 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<!--
|
||||
Despite spellcheck dictionaries being created per-user, IntelliJ loads all of them, per:
|
||||
https://stackoverflow.com/a/28151692
|
||||
-->
|
||||
<dictionary name="bt">
|
||||
<words>
|
||||
<w>arpabet</w>
|
||||
<w>backfill</w>
|
||||
<w>cashapp</w>
|
||||
<w>cheermote</w>
|
||||
<w>fakeyou</w>
|
||||
<w>hifigan</w>
|
||||
|
||||
Generated
+151
@@ -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"
|
||||
|
||||
+7
-1
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE generic_workloads;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
publish = false
|
||||
name = "component_subsystem_billing"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
|
||||
[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"
|
||||
File renamed without changes.
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
publish = false
|
||||
name = "component_subsystem_inference_workloads"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
|
||||
[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"
|
||||
Whitespace-only changes.
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
publish = false
|
||||
name = "users_component"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
|
||||
[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" ] }
|
||||
@@ -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<T, B> (app: App<T, B>) -> App<T, B>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
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()))
|
||||
)
|
||||
}
|
||||
+64
-110
@@ -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<CreateAccountRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, CreateAccountErrorResponse>
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_cookie_manager: web::Data<SessionCookieManager>,
|
||||
firehose_publisher: web::Data<FirehosePublisher>,
|
||||
) -> Result<HttpResponse, CreateAccountErrorResponse>
|
||||
{
|
||||
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)
|
||||
+69
-113
@@ -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<EditProfilePathInfo>,
|
||||
request: web::Json<EditProfileRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EditProfileError>
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_checker: web::Data<SessionChecker>,
|
||||
) -> Result<HttpResponse, EditProfileError>
|
||||
{
|
||||
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")
|
||||
+21
-31
@@ -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<UserProfileRecordForResponse>,
|
||||
}
|
||||
|
||||
#[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<GetProfilePathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, ProfileError>
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_checker: web::Data<SessionChecker>,
|
||||
) -> Result<HttpResponse, ProfileError>
|
||||
{
|
||||
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();
|
||||
|
||||
+20
-68
@@ -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<LoginRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, LoginErrorResponse>
|
||||
session_cookie_manager: web::Data<SessionCookieManager>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
) -> Result<HttpResponse, LoginErrorResponse>
|
||||
{
|
||||
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<u8>,
|
||||
is_banned: i8,
|
||||
}
|
||||
|
||||
async fn lookup_by_username(username: &str, pool: &MySqlPool) -> AnyhowResult<UserRecordForLogin>
|
||||
{
|
||||
// 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<UserRecordForLogin> {
|
||||
// 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)
|
||||
}
|
||||
@@ -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<SessionCookieManager>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
) -> Result<HttpResponse, LogoutError>
|
||||
{
|
||||
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))
|
||||
}
|
||||
@@ -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;
|
||||
+8
-7
@@ -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<Arc<ServerState>>) -> Result<HttpResponse, SessionInfoError>
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_checker: web::Data<SessionChecker>,
|
||||
) -> Result<HttpResponse, SessionInfoError>
|
||||
{
|
||||
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| {
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod email_to_gravatar;
|
||||
pub mod session_checker;
|
||||
pub mod session_cookie_manager;
|
||||
@@ -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<MySql> method instead of the MySqlPool one."]
|
||||
pub async fn maybe_get_session(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
pool: &MySqlPool
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
{
|
||||
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<MySql>,
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
{
|
||||
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<MySql> method instead of the MySqlPool one."]
|
||||
pub async fn maybe_get_user_session(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
{
|
||||
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<MySql>,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+14
-8
@@ -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(),
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
File renamed without changes.
+10
-11
@@ -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]
|
||||
+5
-6
@@ -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());
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+17
-19
@@ -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]
|
||||
+6
-7
@@ -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());
|
||||
}
|
||||
}
|
||||
+1
-24
@@ -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() {
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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<P: AsRef<Path>>(directory_path: P) {
|
||||
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
|
||||
|
||||
@@ -15,6 +15,16 @@ pub fn nullable_i8_to_bool(value: Option<i8>, default_value: bool) -> bool {
|
||||
.unwrap_or(default_value)
|
||||
}
|
||||
|
||||
pub fn nullable_i8_to_bool_default_false(value: Option<i8>) -> 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<i8>) -> Option<bool> {
|
||||
value.map(|v| i8_to_bool(v))
|
||||
@@ -22,11 +32,7 @@ pub fn nullable_i8_to_optional_bool(value: Option<i8>) -> Option<bool> {
|
||||
|
||||
#[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);
|
||||
|
||||
@@ -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<CreateAccountSuccessResult, CreateAccountError>
|
||||
{
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
+72
@@ -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(())
|
||||
}
|
||||
|
||||
+61
@@ -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(())
|
||||
}
|
||||
File renamed without changes.
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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<bool>,
|
||||
pub preferred_tts_result_visibility: RecordVisibility,
|
||||
pub preferred_w2l_result_visibility: RecordVisibility,
|
||||
pub auto_play_video_preference: Option<bool>,
|
||||
|
||||
// ===== 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<MySql>,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>> {
|
||||
|
||||
// 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<i8>,
|
||||
auto_play_video_preference: Option<i8>,
|
||||
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<i8>,
|
||||
can_use_w2l: Option<i8>,
|
||||
can_delete_own_tts_results: Option<i8>,
|
||||
can_delete_own_w2l_results: Option<i8>,
|
||||
can_delete_own_account: Option<i8>,
|
||||
|
||||
// Contribution
|
||||
can_upload_tts_models: Option<i8>,
|
||||
can_upload_w2l_templates: Option<i8>,
|
||||
can_delete_own_tts_models: Option<i8>,
|
||||
can_delete_own_w2l_templates: Option<i8>,
|
||||
|
||||
// Moderation
|
||||
can_approve_w2l_templates: Option<i8>,
|
||||
can_edit_other_users_profiles: Option<i8>,
|
||||
can_edit_other_users_tts_models: Option<i8>,
|
||||
can_edit_other_users_w2l_templates: Option<i8>,
|
||||
can_delete_other_users_tts_models: Option<i8>,
|
||||
can_delete_other_users_tts_results: Option<i8>,
|
||||
can_delete_other_users_w2l_templates: Option<i8>,
|
||||
can_delete_other_users_w2l_results: Option<i8>,
|
||||
can_ban_users: Option<i8>,
|
||||
can_delete_users: Option<i8>,
|
||||
}
|
||||
+51
@@ -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<MySql>,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionRecord>> {
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -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<UserRecordForLogin> {
|
||||
// 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)
|
||||
}
|
||||
+22
@@ -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<UserRecordForLogin>
|
||||
{
|
||||
// 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)
|
||||
}
|
||||
+9
@@ -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<u8>,
|
||||
pub is_banned: i8,
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
pub mod create_session;
|
||||
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;
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
+1
-3
@@ -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>"),
|
||||
"<p><script>alert();</script></p>\n");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
const IP_ADDRESS_REGEX : Lazy<Regex> = 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
[package]
|
||||
publish = false
|
||||
name = "image-generation-service"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
|
||||
# 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"] }
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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<T, B> (app: App<T, B>) -> App<T, B>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
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))
|
||||
}
|
||||
@@ -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<RedisConnectionManager>,
|
||||
|
||||
//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 {
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
-2
@@ -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;
|
||||
|
||||
-2
@@ -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};
|
||||
|
||||
-2
@@ -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};
|
||||
|
||||
+12
-7
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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<AddIpBanRequest>,
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+1
-3
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
pub mod list_tts_models;
|
||||
pub mod list_user_tts_inference_results;
|
||||
pub mod list_user_tts_models;
|
||||
@@ -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<Arc<ServerState>>) -> Result<HttpResponse, LogoutError>
|
||||
{
|
||||
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(())
|
||||
}
|
||||
@@ -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;
|
||||
Whitespace-only changes.
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user