From dba1f662a7aa262bca9c1111d3ba5d1f22809404 Mon Sep 17 00:00:00 2001
From: realaravinth <realaravinth@batsense.net>
Date: Sun, 8 May 2022 20:02:17 +0530
Subject: [PATCH] feat: init postgres implementation via sqlx

---
 Cargo.lock                                    |  25 ++++
 Cargo.toml                                    |  16 ++-
 db/db-sqlx-postgres/.gitignore                |   2 +
 db/db-sqlx-postgres/Cargo.toml                |  20 +++
 .../20210310122154_mcaptcha_users.sql         |   8 ++
 .../20210310122617_mcaptcha_config.sql        |   7 +
 .../20210310122902_mcaptcha_levels.sql        |   6 +
 ...10430032935_mcaptcha_pow_fetched_stats.sql |   4 +
 ...210509135118_mcaptcha_pow_solved_stats.sql |   4 +
 ...509135154_mcaptcha_pow_confirmed_stats.sql |   4 +
 .../20210509151150_mcaptcha_notifications.sql |  10 ++
 ...tcha_sitekey_user_provided_avg_traffic.sql |   6 +
 ...user_provided_avg_traffic_col_datatype.sql |   3 +
 db/db-sqlx-postgres/src/errors.rs             |  40 ++++++
 db/db-sqlx-postgres/src/lib.rs                | 129 ++++++++++++++++++
 db/db-sqlx-postgres/src/tests.rs              |  28 ++++
 16 files changed, 308 insertions(+), 4 deletions(-)
 create mode 100644 db/db-sqlx-postgres/.gitignore
 create mode 100644 db/db-sqlx-postgres/Cargo.toml
 create mode 100644 db/db-sqlx-postgres/migrations/20210310122154_mcaptcha_users.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210310122617_mcaptcha_config.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210310122902_mcaptcha_levels.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210509135118_mcaptcha_pow_solved_stats.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20210509151150_mcaptcha_notifications.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20211202141927_mcaptcha_sitekey_user_provided_avg_traffic.sql
 create mode 100644 db/db-sqlx-postgres/migrations/20211218133703_change_user_provided_avg_traffic_col_datatype.sql
 create mode 100644 db/db-sqlx-postgres/src/errors.rs
 create mode 100644 db/db-sqlx-postgres/src/lib.rs
 create mode 100644 db/db-sqlx-postgres/src/tests.rs

diff --git a/Cargo.lock b/Cargo.lock
index 3e3eee7d..97cd4f59 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -835,6 +835,28 @@ version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
 
+[[package]]
+name = "db-core"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "serde 1.0.137",
+ "serde_json",
+ "thiserror",
+ "url",
+]
+
+[[package]]
+name = "db-sqlx-postgres"
+version = "0.1.0"
+dependencies = [
+ "actix-rt",
+ "async-trait",
+ "db-core",
+ "sqlx",
+ "url",
+]
+
 [[package]]
 name = "derive_builder"
 version = "0.10.2"
@@ -1629,6 +1651,8 @@ dependencies = [
  "awc",
  "cache-buster",
  "config",
+ "db-core",
+ "db-sqlx-postgres",
  "derive_builder 0.11.1",
  "derive_more",
  "futures",
@@ -3195,6 +3219,7 @@ dependencies = [
  "idna",
  "matches",
  "percent-encoding",
+ "serde 1.0.137",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index bf3b2945..710dfd89 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,14 +13,15 @@ build = "build.rs"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+
+[workspace]
+exclude = ["db/db-migrations"]
+memebers = [".", "db/db-core", "db/db-sqlx-postgres"]
+
 [[bin]]
 name = "mcaptcha"
 path = "./src/main.rs"
 
-[[bin]]
-name = "tests-migrate"
-path = "./src/tests-migrate.rs"
-
 [dependencies]
 actix-web = "4.0.1"
 actix = "0.13"
@@ -76,6 +77,13 @@ lettre = { version = "0.10.0-rc.3", features = [
 
 openssl = { version = "0.10.29", features = ["vendored"] }
 
+
+[dependencies.db-core]
+path = "./db/db-core"
+
+[dependencies.db-sqlx-postgres]
+path = "./db/db-sqlx-postgres"
+
 [dependencies.my-codegen]
 git = "https://github.com/realaravinth/actix-web"
 package = "actix-web-codegen"
diff --git a/db/db-sqlx-postgres/.gitignore b/db/db-sqlx-postgres/.gitignore
new file mode 100644
index 00000000..4fffb2f8
--- /dev/null
+++ b/db/db-sqlx-postgres/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/db/db-sqlx-postgres/Cargo.toml b/db/db-sqlx-postgres/Cargo.toml
new file mode 100644
index 00000000..350aac87
--- /dev/null
+++ b/db/db-sqlx-postgres/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "db-sqlx-postgres"
+version = "0.1.0"
+edition = "2021"
+homepage = "https://mcaptcha.org"
+repository = "https://github.com/mCaptcha/mCaptcha"
+documentation = "https://mcaptcha.org/docs/"
+license = "AGPLv3 or later version"
+authors = ["realaravinth <realaravinth@batsense.net>"]
+
+[dependencies]
+sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
+db-core = {path = "../db-core"}
+async-trait = "0.1.51"
+
+[dev-dependencies]
+actix-rt = "2"
+sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
+db-core = {path = "../db-core", features = ["test"]}
+url = { version  = "2.2.2", features = ["serde"] }
diff --git a/db/db-sqlx-postgres/migrations/20210310122154_mcaptcha_users.sql b/db/db-sqlx-postgres/migrations/20210310122154_mcaptcha_users.sql
new file mode 100644
index 00000000..79e230aa
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210310122154_mcaptcha_users.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_users (
+	name VARCHAR(100) NOT NULL UNIQUE,
+	email VARCHAR(100) UNIQUE DEFAULT NULL,
+	email_verified BOOLEAN DEFAULT NULL,
+    secret varchar(50) NOT NULL UNIQUE,
+	password TEXT NOT NULL,
+	ID SERIAL PRIMARY KEY NOT NULL
+);
diff --git a/db/db-sqlx-postgres/migrations/20210310122617_mcaptcha_config.sql b/db/db-sqlx-postgres/migrations/20210310122617_mcaptcha_config.sql
new file mode 100644
index 00000000..ba2dc9d6
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210310122617_mcaptcha_config.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_config (
+	config_id SERIAL PRIMARY KEY NOT NULL,
+	user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
+	key varchar(100) NOT NULL UNIQUE,
+	name varchar(100) NOT NULL,
+	duration integer NOT NULL DEFAULT 30
+);
diff --git a/db/db-sqlx-postgres/migrations/20210310122902_mcaptcha_levels.sql b/db/db-sqlx-postgres/migrations/20210310122902_mcaptcha_levels.sql
new file mode 100644
index 00000000..e04963ac
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210310122902_mcaptcha_levels.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_levels (
+	config_id INTEGER references mcaptcha_config(config_id)  ON DELETE CASCADE,
+	difficulty_factor INTEGER NOT NULL,
+	visitor_threshold INTEGER NOT NULL,
+	level_id SERIAL PRIMARY KEY NOT NULL
+);
diff --git a/db/db-sqlx-postgres/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql b/db/db-sqlx-postgres/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql
new file mode 100644
index 00000000..0a770f42
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210430032935_mcaptcha_pow_fetched_stats.sql
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_pow_fetched_stats (
+	config_id INTEGER references mcaptcha_config(config_id)  ON DELETE CASCADE,
+	time timestamptz NOT NULL DEFAULT now()
+);
diff --git a/db/db-sqlx-postgres/migrations/20210509135118_mcaptcha_pow_solved_stats.sql b/db/db-sqlx-postgres/migrations/20210509135118_mcaptcha_pow_solved_stats.sql
new file mode 100644
index 00000000..1f9d7214
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210509135118_mcaptcha_pow_solved_stats.sql
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_pow_solved_stats (
+	config_id INTEGER references mcaptcha_config(config_id)  ON DELETE CASCADE,
+	time timestamptz NOT NULL DEFAULT now()
+);
diff --git a/db/db-sqlx-postgres/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql b/db/db-sqlx-postgres/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql
new file mode 100644
index 00000000..348c7008
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210509135154_mcaptcha_pow_confirmed_stats.sql
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_pow_confirmed_stats (
+	config_id INTEGER references mcaptcha_config(config_id)  ON DELETE CASCADE,
+	time timestamptz NOT NULL DEFAULT now()
+);
diff --git a/db/db-sqlx-postgres/migrations/20210509151150_mcaptcha_notifications.sql b/db/db-sqlx-postgres/migrations/20210509151150_mcaptcha_notifications.sql
new file mode 100644
index 00000000..56e5e733
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20210509151150_mcaptcha_notifications.sql
@@ -0,0 +1,10 @@
+-- Add migration script here
+CREATE TABLE IF NOT EXISTS mcaptcha_notifications (
+	id SERIAL PRIMARY KEY NOT NULL,
+	tx INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
+	rx INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
+	heading varchar(30) NOT NULL,
+	message varchar(250) NOT NULL,
+	read BOOLEAN DEFAULT NULL,
+	received timestamptz NOT NULL DEFAULT now()
+);
diff --git a/db/db-sqlx-postgres/migrations/20211202141927_mcaptcha_sitekey_user_provided_avg_traffic.sql b/db/db-sqlx-postgres/migrations/20211202141927_mcaptcha_sitekey_user_provided_avg_traffic.sql
new file mode 100644
index 00000000..0c8697e2
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20211202141927_mcaptcha_sitekey_user_provided_avg_traffic.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS mcaptcha_sitekey_user_provided_avg_traffic (
+	config_id INTEGER PRIMARY KEY UNIQUE NOT NULL references mcaptcha_config(config_id)  ON DELETE CASCADE,
+	avg_traffic INTEGER DEFAULT NULL,
+	peak_sustainable_traffic INTEGER DEFAULT NULL,
+	broke_my_site_traffic INTEGER DEFAULT NULL
+);
diff --git a/db/db-sqlx-postgres/migrations/20211218133703_change_user_provided_avg_traffic_col_datatype.sql b/db/db-sqlx-postgres/migrations/20211218133703_change_user_provided_avg_traffic_col_datatype.sql
new file mode 100644
index 00000000..4d2aa195
--- /dev/null
+++ b/db/db-sqlx-postgres/migrations/20211218133703_change_user_provided_avg_traffic_col_datatype.sql
@@ -0,0 +1,3 @@
+ALTER TABLE mcaptcha_sitekey_user_provided_avg_traffic 
+	ALTER COLUMN avg_traffic SET NOT NULL,
+	ALTER COLUMN peak_sustainable_traffic SET NOT NULL;
diff --git a/db/db-sqlx-postgres/src/errors.rs b/db/db-sqlx-postgres/src/errors.rs
new file mode 100644
index 00000000..74ea9484
--- /dev/null
+++ b/db/db-sqlx-postgres/src/errors.rs
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! Error-handling utilities
+use std::borrow::Cow;
+
+use db_core::dev::*;
+use sqlx::Error;
+
+/// map postgres errors to [DBError](DBError) types
+pub fn map_register_err(e: Error) -> DBError {
+    if let Error::Database(err) = e {
+        if err.code() == Some(Cow::from("23505")) {
+            let msg = err.message();
+            if msg.contains("mcaptcha_users_username_key") {
+                unimplemented!();
+            } else {
+                DBError::DBError(Box::new(Error::Database(err)))
+            }
+        } else {
+            DBError::DBError(Box::new(Error::Database(err)))
+        }
+    } else {
+        DBError::DBError(Box::new(e))
+    }
+}
diff --git a/db/db-sqlx-postgres/src/lib.rs b/db/db-sqlx-postgres/src/lib.rs
new file mode 100644
index 00000000..00bfb6a9
--- /dev/null
+++ b/db/db-sqlx-postgres/src/lib.rs
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+use db_core::dev::*;
+use std::str::FromStr;
+
+use sqlx::postgres::PgPoolOptions;
+use sqlx::PgPool;
+use sqlx::types::time::OffsetDateTime;
+
+pub mod errors;
+#[cfg(test)]
+pub mod tests;
+
+#[derive(Clone)]
+pub struct Database {
+    pub pool: PgPool,
+}
+
+/// Use an existing database pool
+pub struct Conn(pub PgPool);
+
+/// Connect to databse
+pub enum ConnectionOptions {
+    /// fresh connection
+    Fresh(Fresh),
+    /// existing connection
+    Existing(Conn),
+}
+
+pub struct Fresh {
+    pub pool_options: PgPoolOptions,
+    pub url: String,
+}
+
+pub mod dev {
+    pub use super::errors::*;
+    pub use super::Database;
+    pub use db_core::dev::*;
+    pub use prelude::*;
+    pub use sqlx::Error;
+}
+
+pub mod prelude {
+    pub use super::*;
+    pub use db_core::prelude::*;
+}
+
+#[async_trait]
+impl Connect for ConnectionOptions {
+    type Pool = Database;
+    async fn connect(self) -> DBResult<Self::Pool> {
+        let pool = match self {
+            Self::Fresh(fresh) => fresh
+                .pool_options
+                .connect(&fresh.url)
+                .await
+                .map_err(|e| DBError::DBError(Box::new(e)))?,
+            Self::Existing(conn) => conn.0,
+        };
+        Ok(Database { pool })
+    }
+}
+
+use dev::*;
+
+#[async_trait]
+impl Migrate for Database {
+    async fn migrate(&self) -> DBResult<()> {
+        sqlx::migrate!("./migrations/")
+            .run(&self.pool)
+            .await
+            .map_err(|e| DBError::DBError(Box::new(e)))?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl MCDatabase for Database {
+    /// ping DB
+    async fn ping(&self) -> bool {
+        use sqlx::Connection;
+
+        if let Ok(mut con) = self.pool.acquire().await {
+            con.ping().await.is_ok()
+        } else {
+            false
+        }
+    }
+}
+
+fn now_unix_time_stamp() -> i64 {
+    OffsetDateTime::now_utc().unix_timestamp()
+}
+
+//
+//#[allow(non_snake_case)]
+//struct InnerGistComment {
+//    ID: i64,
+//    owner: String,
+//    comment: Option<String>,
+//    gist_public_id: String,
+//    created: i64,
+//}
+//
+//impl From<InnerGistComment> for GistComment {
+//    fn from(g: InnerGistComment) -> Self {
+//        Self {
+//            id: g.ID,
+//            owner: g.owner,
+//            comment: g.comment.unwrap(),
+//            gist_public_id: g.gist_public_id,
+//            created: g.created,
+//        }
+//    }
+//}
diff --git a/db/db-sqlx-postgres/src/tests.rs b/db/db-sqlx-postgres/src/tests.rs
new file mode 100644
index 00000000..6d1624db
--- /dev/null
+++ b/db/db-sqlx-postgres/src/tests.rs
@@ -0,0 +1,28 @@
+
+/*
+ * Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+//use sqlx::postgres::PgPoolOptions;
+//use std::env;
+//
+//use crate::*;
+//
+//use db_core::tests::*;
+//
+//#[actix_rt::test]
+//async fn everyting_works() {
+//    unimplemented!();
+//}