Merge pull request #9 from storytold/echelon-billing-and-images

Pull user system into a reusable component library
This commit is contained in:
Brandon Thomas authored and GitHub committed 2022-09-01 23:40:32 -04:00
commit bfe4b8d090
120 files changed
+3216 -1775

No files matched your search

+1 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -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"
@@ -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()))
)
}
@@ -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)
@@ -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")
@@ -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();
@@ -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;
@@ -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
}
}
@@ -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(),
@@ -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;
@@ -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]
@@ -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());
}
}
@@ -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());
}
}
@@ -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]
@@ -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,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;
@@ -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(())
}
@@ -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(())
}
@@ -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>,
}
@@ -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))
}
}
}
}
}
@@ -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)
}
@@ -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)
}
@@ -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;
+1
View File
@@ -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"
+2
View File
@@ -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,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>&lt;script&gt;alert();&lt;/script&gt;</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" }
@@ -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;
@@ -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};
@@ -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;
@@ -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;
@@ -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};
@@ -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};
@@ -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()));
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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};
@@ -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};
@@ -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;
@@ -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