mirror of https://github.com/realaravinth/gitpad
feat: bootstrap database ops
- db-core: defines base database traits that are required for gists - db-sqlx-postgres: implements db-core for postgres flavor of the sqlx library - db-sqlx-sqlite: implements db-core for sqlite flavor of the sqlx librarymaster
parent
4858f050e7
commit
b28a7d0cfb
@ -0,0 +1,3 @@
|
||||
export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"
|
||||
export SQLITE_TMP="$(pwd)/database/db-sqlx-sqlite/tmp"
|
||||
export SQLITE_DATABASE_URL="sqlite://$SQLITE_TMP/admin.db"
|
@ -1 +1,2 @@
|
||||
/target
|
||||
.env
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "db-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/realaravinth/gists"
|
||||
repository = "https://github.com/realaravinth/gists"
|
||||
documentation = "https://github.con/realaravinth/gists"
|
||||
readme = "https://github.com/realaravinth/gists/blob/master/README.md"
|
||||
license = "AGPLv3 or later version"
|
||||
authors = ["realaravinth <realaravinth@batsense.net>"]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.51"
|
||||
thiserror = "1.0.30"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test = []
|
@ -0,0 +1,38 @@
|
||||
//! represents all the ways a trait can fail using this crate
|
||||
use std::error::Error as StdError;
|
||||
|
||||
//use derive_more::{error, Error as DeriveError};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error data structure grouping various error subtypes
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DBError {
|
||||
/// username is already taken
|
||||
#[error("Username not available")]
|
||||
DuplicateUsername,
|
||||
|
||||
/// user secret is already taken
|
||||
#[error("User secret not available")]
|
||||
DuplicateSecret,
|
||||
|
||||
/// email is already taken
|
||||
#[error("Email not available")]
|
||||
DuplicateEmail,
|
||||
|
||||
/// Account with specified characteristics not found
|
||||
#[error("Account with specified characteristics not found")]
|
||||
AccountNotFound,
|
||||
|
||||
// /// errors that are specific to a database implementation
|
||||
// #[error("Database error: {:?}", _0)]
|
||||
// DBError(#[error(not(source))] String),
|
||||
/// errors that are specific to a database implementation
|
||||
#[error("{0}")]
|
||||
DBError(#[source] BoxDynError),
|
||||
}
|
||||
|
||||
/// Convenience type alias for grouping driver-specific errors
|
||||
pub type BoxDynError = Box<dyn StdError + 'static + Send + Sync>;
|
||||
|
||||
/// Generic result data structure
|
||||
pub type DBResult<V> = std::result::Result<V, DBError>;
|
@ -0,0 +1,172 @@
|
||||
#![warn(missing_docs)]
|
||||
//! # `gists` database operations
|
||||
//!
|
||||
//! Traits and datastructures used in gists to interact with database.
|
||||
//!
|
||||
//! To use an unsupported database with gists, traits present within this crate should be
|
||||
//! implemented.
|
||||
//!
|
||||
//!
|
||||
//! ## Organisation
|
||||
//!
|
||||
//! Database functionallity is divided accross various modules:
|
||||
//!
|
||||
//! - [errors](crate::auth): error data structures used in this crate
|
||||
//! - [ops](crate::ops): meta operations like connection pool creation, migrations and getting
|
||||
//! connection from pool
|
||||
pub mod errors;
|
||||
pub mod ops;
|
||||
#[cfg(feature = "test")]
|
||||
pub mod tests;
|
||||
|
||||
pub use ops::GetConnection;
|
||||
|
||||
pub mod prelude {
|
||||
//! useful imports for users working with a supported database
|
||||
|
||||
pub use super::errors::*;
|
||||
pub use super::ops::*;
|
||||
pub use super::*;
|
||||
}
|
||||
|
||||
pub mod dev {
|
||||
//! useful imports for supporting a new database
|
||||
pub use super::prelude::*;
|
||||
pub use async_trait::async_trait;
|
||||
}
|
||||
|
||||
/// data structure describing credentials of a user
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Creds {
|
||||
/// username
|
||||
pub username: String,
|
||||
/// password
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// data structure containing only a password field
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Password {
|
||||
/// password
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// payload to register a user with username _and_ email
|
||||
pub struct EmailRegisterPayload<'a> {
|
||||
/// username of new user
|
||||
pub username: &'a str,
|
||||
/// password of new user
|
||||
pub password: &'a str,
|
||||
/// password of new user
|
||||
pub email: &'a str,
|
||||
/// a randomly generated secret associated with an account
|
||||
pub secret: &'a str,
|
||||
}
|
||||
|
||||
/// payload to register a user with only username
|
||||
pub struct UsernameRegisterPayload<'a> {
|
||||
/// username provided during registration
|
||||
pub username: &'a str,
|
||||
/// password of new user
|
||||
pub password: &'a str,
|
||||
/// a randomly generated secret associated with an account
|
||||
pub secret: &'a str,
|
||||
}
|
||||
|
||||
/// payload to update email in the database
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpdateEmailPayload<'a> {
|
||||
/// name of the user who's email is to be updated
|
||||
pub username: &'a str,
|
||||
/// new email
|
||||
pub email: &'a str,
|
||||
}
|
||||
|
||||
/// payload to update a username in database
|
||||
pub struct UpdateUsernamePayload<'a> {
|
||||
/// old usename
|
||||
pub old_username: &'a str,
|
||||
/// new username
|
||||
pub new_username: &'a str,
|
||||
}
|
||||
|
||||
use dev::*;
|
||||
/// foo
|
||||
#[async_trait]
|
||||
pub trait GistDatabase: std::marker::Send + std::marker::Sync {
|
||||
/// Update email of specified user in database
|
||||
async fn update_email(&self, payload: &UpdateEmailPayload) -> DBResult<()>;
|
||||
/// Update password of specified user in database
|
||||
async fn update_password(&self, payload: &Creds) -> DBResult<()>;
|
||||
/// check if an email exists in the database
|
||||
async fn email_exists(&self, email: &str) -> DBResult<bool>;
|
||||
/// delete account from database
|
||||
async fn delete_account(&self, username: &str) -> DBResult<()>;
|
||||
/// check if a username exists in the database
|
||||
async fn username_exists(&self, username: &str) -> DBResult<bool>;
|
||||
/// update username in database
|
||||
async fn update_username(&self, payload: &UpdateUsernamePayload) -> DBResult<()>;
|
||||
/// update secret in database
|
||||
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()>;
|
||||
/// update secret in database
|
||||
async fn get_secret(&self, username: &str) -> DBResult<String>;
|
||||
/// login with email as user-identifier
|
||||
async fn email_login(&self, email: &str) -> DBResult<Creds>;
|
||||
/// login with username as user-identifier
|
||||
async fn username_login(&self, username: &str) -> DBResult<Password>;
|
||||
/// username _and_ email is available during registration
|
||||
async fn email_register(&self, payload: &EmailRegisterPayload) -> DBResult<()>;
|
||||
/// register with username
|
||||
async fn username_register(&self, payload: &UsernameRegisterPayload) -> DBResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: GistDatabase + ?Sized> GistDatabase for Box<T> {
|
||||
async fn update_email(&self, payload: &UpdateEmailPayload) -> DBResult<()> {
|
||||
(**self).update_email(payload).await
|
||||
}
|
||||
/// Update password of specified user in database
|
||||
async fn update_password(&self, payload: &Creds) -> DBResult<()> {
|
||||
(**self).update_password(payload).await
|
||||
}
|
||||
/// check if an email exists in the database
|
||||
async fn email_exists(&self, email: &str) -> DBResult<bool> {
|
||||
(**self).email_exists(email).await
|
||||
}
|
||||
/// delete account from database
|
||||
async fn delete_account(&self, username: &str) -> DBResult<()> {
|
||||
(**self).delete_account(username).await
|
||||
}
|
||||
/// check if a username exists in the database
|
||||
async fn username_exists(&self, username: &str) -> DBResult<bool> {
|
||||
(**self).username_exists(username).await
|
||||
}
|
||||
/// update username in database
|
||||
async fn update_username(&self, payload: &UpdateUsernamePayload) -> DBResult<()> {
|
||||
(**self).update_username(payload).await
|
||||
}
|
||||
/// update secret in database
|
||||
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
|
||||
(**self).update_secret(username, secret).await
|
||||
}
|
||||
/// update secret in database
|
||||
async fn get_secret(&self, username: &str) -> DBResult<String> {
|
||||
(**self).get_secret(username).await
|
||||
}
|
||||
/// login with email as user-identifier
|
||||
async fn email_login(&self, email: &str) -> DBResult<Creds> {
|
||||
(**self).email_login(email).await
|
||||
}
|
||||
/// login with username as user-identifier
|
||||
async fn username_login(&self, username: &str) -> DBResult<Password> {
|
||||
(**self).username_login(username).await
|
||||
}
|
||||
/// username _and_ email is available during registration
|
||||
async fn email_register(&self, payload: &EmailRegisterPayload) -> DBResult<()> {
|
||||
(**self).email_register(payload).await
|
||||
}
|
||||
/// register with username
|
||||
async fn username_register(&self, payload: &UsernameRegisterPayload) -> DBResult<()> {
|
||||
(**self).username_register(payload).await
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
//! meta operations like migration and connecting to a database
|
||||
use crate::dev::*;
|
||||
|
||||
/// Database operations trait(migrations, pool creation and fetching connection from pool)
|
||||
pub trait DBOps: GetConnection + Migrate {}
|
||||
|
||||
/// Get database connection
|
||||
#[async_trait]
|
||||
pub trait GetConnection {
|
||||
/// database connection type
|
||||
type Conn;
|
||||
/// database specific error-type
|
||||
/// get connection from connection pool
|
||||
async fn get_conn(&self) -> DBResult<Self::Conn>;
|
||||
}
|
||||
|
||||
/// Create databse connection
|
||||
#[async_trait]
|
||||
pub trait Connect {
|
||||
/// database specific pool-type
|
||||
type Pool: GistDatabase;
|
||||
/// database specific error-type
|
||||
/// create connection pool
|
||||
async fn connect(self) -> DBResult<Self::Pool>;
|
||||
}
|
||||
|
||||
/// database migrations
|
||||
#[async_trait]
|
||||
pub trait Migrate: GistDatabase {
|
||||
/// database specific error-type
|
||||
/// run migrations
|
||||
async fn migrate(&self) -> DBResult<()>;
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
//! Test utilities
|
||||
use crate::prelude::*;
|
||||
|
||||
/// test email registration implementation
|
||||
pub async fn email_register_works<T: GistDatabase>(
|
||||
db: &T,
|
||||
email: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
secret: &str,
|
||||
username2: &str,
|
||||
) {
|
||||
let _ = db.delete_account(username).await;
|
||||
let _ = db.delete_account(username2).await;
|
||||
|
||||
assert!(matches!(
|
||||
db.email_login(email).await.err(),
|
||||
Some(DBError::AccountNotFound)
|
||||
));
|
||||
|
||||
let mut register_payload = EmailRegisterPayload {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
secret,
|
||||
};
|
||||
|
||||
db.email_register(®ister_payload).await.unwrap();
|
||||
assert!(db.username_exists(username).await.unwrap());
|
||||
assert!(db.email_exists(email).await.unwrap());
|
||||
assert_eq!(db.get_secret(username).await.unwrap(), secret);
|
||||
let login_resp = db.email_login(email).await.unwrap();
|
||||
assert_eq!(login_resp.username, username);
|
||||
assert_eq!(login_resp.password, password);
|
||||
|
||||
register_payload.secret = email;
|
||||
register_payload.username = username2;
|
||||
let err = db.email_register(®ister_payload).await.err();
|
||||
assert!(matches!(err, Some(DBError::DuplicateEmail)));
|
||||
}
|
||||
|
||||
/// test username registration implementation
|
||||
pub async fn username_register_works<T: GistDatabase>(
|
||||
db: &T,
|
||||
username: &str,
|
||||
password: &str,
|
||||
secret: &str,
|
||||
) {
|
||||
let _ = db.delete_account(username).await;
|
||||
assert!(matches!(
|
||||
db.username_login(username).await.err(),
|
||||
Some(DBError::AccountNotFound)
|
||||
));
|
||||
|
||||
let mut register_payload = UsernameRegisterPayload {
|
||||
username,
|
||||
password,
|
||||
secret,
|
||||
};
|
||||
|
||||
db.username_register(®ister_payload).await.unwrap();
|
||||
assert!(db.username_exists(username).await.unwrap());
|
||||
assert_eq!(db.get_secret(username).await.unwrap(), secret);
|
||||
let login_resp = db.username_login(username).await.unwrap();
|
||||
assert_eq!(login_resp.password, password);
|
||||
|
||||
register_payload.secret = username;
|
||||
assert!(matches!(
|
||||
db.username_register(®ister_payload).await.err(),
|
||||
Some(DBError::DuplicateUsername)
|
||||
));
|
||||
}
|
||||
|
||||
/// test duplicate secret errors
|
||||
pub async fn duplicate_secret_guard_works<T: GistDatabase>(
|
||||
db: &T,
|
||||
username: &str,
|
||||
password: &str,
|
||||
username2: &str,
|
||||
secret: &str,
|
||||
duplicate_secret: &str,
|
||||
) {
|
||||
let _ = db.delete_account(username).await;
|
||||
let _ = db.delete_account(username2).await;
|
||||
|
||||
let mut register_payload = UsernameRegisterPayload {
|
||||
username,
|
||||
password,
|
||||
secret,
|
||||
};
|
||||
|
||||
db.username_register(®ister_payload).await.unwrap();
|
||||
assert!(db.username_exists(username).await.unwrap());
|
||||
assert_eq!(db.get_secret(username).await.unwrap(), secret);
|
||||
|
||||
register_payload.username = username2;
|
||||
assert!(matches!(
|
||||
db.username_register(®ister_payload).await.err(),
|
||||
Some(DBError::DuplicateSecret)
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
db.update_secret(username, duplicate_secret).await.err(),
|
||||
Some(DBError::DuplicateSecret)
|
||||
));
|
||||
|
||||
db.update_secret(username, username).await.unwrap();
|
||||
}
|
||||
|
||||
/// check if duplicate username and duplicate email guards are working on update workflows
|
||||
|
||||
pub async fn duplicate_username_and_email<T: GistDatabase>(
|
||||
db: &T,
|
||||
username: &str,
|
||||
fresh_username: &str,
|
||||
fresh_email: &str,
|
||||
password: &str,
|
||||
secret: &str,
|
||||
duplicate_username: &str,
|
||||
duplicate_email: &str,
|
||||
) {
|
||||
let _ = db.delete_account(username).await;
|
||||
let _ = db.delete_account(fresh_username).await;
|
||||
let register_payload = UsernameRegisterPayload {
|
||||
username,
|
||||
password,
|
||||
secret,
|
||||
};
|
||||
|
||||
db.username_register(®ister_payload).await.unwrap();
|
||||
|
||||
let mut update_email_payload = UpdateEmailPayload {
|
||||
username,
|
||||
email: duplicate_email,
|
||||
};
|
||||
let err = db.update_email(&update_email_payload).await.err();
|
||||
assert!(matches!(err, Some(DBError::DuplicateEmail)));
|
||||
update_email_payload.email = fresh_email;
|
||||
db.update_email(&update_email_payload).await.unwrap();
|
||||
|
||||
let mut update_username_payload = UpdateUsernamePayload {
|
||||
new_username: duplicate_username,
|
||||
old_username: username,
|
||||
};
|
||||
assert!(matches!(
|
||||
db.update_username(&update_username_payload).await.err(),
|
||||
Some(DBError::DuplicateUsername)
|
||||
));
|
||||
update_username_payload.new_username = fresh_username;
|
||||
db.update_username(&update_username_payload).await.unwrap();
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "db-sqlx-postgres"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/realaravinth/gists"
|
||||
repository = "https://github.com/realaravinth/gists"
|
||||
documentation = "https://github.con/realaravinth/gists"
|
||||
readme = "https://github.com/realaravinth/gists/blob/master/README.md"
|
||||
license = "AGPLv3 or later version"
|
||||
authors = ["realaravinth <realaravinth@batsense.net>"]
|
||||
include = ["./mgrations/"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
db-core = {path = "../db-core"}
|
||||
sqlx = { version = "0.5.10", features = [ "postgres", "time", "offline" ] }
|
||||
async-trait = "0.1.51"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2"
|
||||
sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
|
||||
db-core = {path = "../db-core", features = ["test"]}
|
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
username 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
|
||||
);
|
@ -0,0 +1,202 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"08e90ce06e795bdb9bf0ea5b18df3b69b107764ad9d224de601b3588fbeac211": {
|
||||
"query": "UPDATE admin_users set secret = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"150c8d182ca90bd50fdd419e5b1b2bb48c8eb5d060d7ab0207dfc04e8eda6fee": {
|
||||
"query": "INSERT INTO admin_users \n (username , password, secret) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"1c6e0ac5913665e512c5d6c8b99af61e6adbac6482de651ff8aeab4210ba4120": {
|
||||
"query": "UPDATE admin_users set email = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"29f35f75a0ddedaabdfecd6a22ee16747f91b2b928641361fe43738eb21e7607": {
|
||||
"query": "DELETE FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"2f9e7d2c4e5335a1c221f8162f5c47c44efbea37cc41670873303f6892e62375": {
|
||||
"query": "UPDATE admin_users set password = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"3e91a474c261bae1ca27f1a5fcf9a276b5fa000039f5118b6daf5640b8708894": {
|
||||
"query": "SELECT secret FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "secret",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"4899a9845675082afcfca09d016da313f09932882f5e71ca7da26db94aa14dc1": {
|
||||
"query": "UPDATE admin_users set username = $1 WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"524065157b7c8e0a2ff02eb8efd7ddf79f585b2f1428f9d2bab92b3d2b2b9b71": {
|
||||
"query": "insert into admin_users \n (username , password, email, secret) values ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"583d7cf4a5a111471f7898bea0001c2e4c18200d8840456a3b28fedf6d7c1359": {
|
||||
"query": "SELECT EXISTS (SELECT 1 from admin_users WHERE username = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"677c618882d6b7e621f9f2b040671d8260518383796efad120a8ffdb596e9d7a": {
|
||||
"query": "SELECT username, password FROM admin_users WHERE email = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "password",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"9b53b26143e9583dac3454eafb34fdfb5976144b64cdc0504138b74f8b950ccd": {
|
||||
"query": "SELECT EXISTS (SELECT 1 from admin_users WHERE email = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"dadfd47b8d33b0b636a79716afa5c8afdf9873304ff9fe883de7eae3aa5e8504": {
|
||||
"query": "SELECT password FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "password",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
//! 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("admin_users_username_key") {
|
||||
DBError::DuplicateUsername
|
||||
} else if msg.contains("admin_users_email_key") {
|
||||
DBError::DuplicateEmail
|
||||
} else if msg.contains("admin_users_secret_key") {
|
||||
DBError::DuplicateSecret
|
||||
} else {
|
||||
DBError::DBError(Box::new(Error::Database(err)))
|
||||
}
|
||||
} else {
|
||||
DBError::DBError(Box::new(Error::Database(err)))
|
||||
}
|
||||
} else {
|
||||
DBError::DBError(Box::new(e))
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
#![deny(missing_docs)]
|
||||
//! # `libadmin` database operations implemented using sqlx postgres
|
||||
//!
|
||||
//! [`GistDatabase`](GistDatabase) is implemented on [Database].
|
||||
|
||||
use db_core::dev::*;
|
||||
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::PgPool;
|
||||
|
||||
mod errors;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
/// Database pool. All database functionallity(`libadmin` traits) are implemented on this
|
||||
/// data structure
|
||||
pub struct Database {
|
||||
/// database pool
|
||||
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),
|
||||
}
|
||||
|
||||
/// Create a new database pool
|
||||
pub struct Fresh {
|
||||
/// Pool options
|
||||
pub pool_options: PgPoolOptions,
|
||||
/// database URL
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
pub mod dev {
|
||||
//! useful imports for supporting a new database
|
||||
pub use super::errors::*;
|
||||
pub use super::Database;
|
||||
pub use db_core::dev::*;
|
||||
pub use prelude::*;
|
||||
pub use sqlx::Error;
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
//! useful imports for users working with a supported database
|
||||
pub use super::*;
|
||||
pub use db_core::prelude::*;
|
||||
}
|
||||
use dev::*;
|
||||
|
||||
#[async_trait]
|
||||
impl Connect for ConnectionOptions {
|
||||
type Pool = Database;
|
||||
/// create connection pool
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
#[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 GistDatabase for Database {
|
||||
async fn email_login(&self, email: &str) -> DBResult<Creds> {
|
||||
sqlx::query_as!(
|
||||
Creds,
|
||||
r#"SELECT username, password FROM admin_users WHERE email = ($1)"#,
|
||||
email,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::RowNotFound => DBError::AccountNotFound,
|
||||
e => DBError::DBError(Box::new(e)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn username_login(&self, username: &str) -> DBResult<Password> {
|
||||
sqlx::query_as!(
|
||||
Password,
|
||||
r#"SELECT password FROM admin_users WHERE username = ($1)"#,
|
||||
username,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::RowNotFound => DBError::AccountNotFound,
|
||||
e => DBError::DBError(Box::new(e)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn email_register(&self, payload: &EmailRegisterPayload) -> DBResult<()> {
|
||||
sqlx::query!(
|
||||
"insert into admin_users
|
||||
(username , password, email, secret) values ($1, $2, $3, $4)",
|
||||
&payload.username,
|
||||
&payload.password,
|
||||
&payload.email,
|
||||
&payload.secret,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn username_register(&self, payload: &UsernameRegisterPayload) -> DBResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO admin_users
|
||||
(username , password, secret) VALUES ($1, $2, $3)",
|
||||
&payload.username,
|
||||
&payload.password,
|
||||
&payload.secret,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_email(&self, payload: &UpdateEmailPayload) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set email = $1
|
||||
WHERE username = $2",
|
||||
&payload.email,
|
||||
&payload.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
if x.rows_affected() == 0 {
|
||||
return Err(DBError::AccountNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn update_password(&self, payload: &Creds) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set password = $1
|
||||
WHERE username = $2",
|
||||
&payload.password,
|
||||
&payload.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?;
|
||||
if x.rows_affected() == 0 {
|
||||
return Err(DBError::AccountNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn email_exists(&self, email: &str) -> DBResult<bool> {
|
||||
let res = sqlx::query!(
|
||||
"SELECT EXISTS (SELECT 1 from admin_users WHERE email = $1)",
|
||||
&email
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?;
|
||||
|
||||
let mut exists = false;
|
||||
if let Some(x) = res.exists {
|
||||
if x {
|
||||
exists = true;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn delete_account(&self, username: &str) -> DBResult<()> {
|
||||
sqlx::query!("DELETE FROM admin_users WHERE username = ($1)", username,)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn username_exists(&self, username: &str) -> DBResult<bool> {
|
||||
let res = sqlx::query!(
|
||||
"SELECT EXISTS (SELECT 1 from admin_users WHERE username = $1)",
|
||||
&username
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?;
|
||||
|
||||
let mut exists = false;
|
||||
if let Some(x) = res.exists {
|
||||
if x {
|
||||
exists = true;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn update_username(&self, payload: &UpdateUsernamePayload) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set username = $1 WHERE username = $2",
|
||||
&payload.new_username,
|
||||
&payload.old_username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
if x.rows_affected() == 0 {
|
||||
return Err(DBError::AccountNotFound);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set secret = $1
|
||||
WHERE username = $2",
|
||||
secret,
|
||||
username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
if x.rows_affected() == 0 {
|
||||
return Err(DBError::AccountNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_secret(&self, username: &str) -> DBResult<String> {
|
||||
struct Secret {
|
||||
secret: String,
|
||||
}
|
||||
let secret = sqlx::query_as!(
|
||||
Secret,
|
||||
r#"SELECT secret FROM admin_users WHERE username = ($1)"#,
|
||||
username,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::RowNotFound => DBError::AccountNotFound,
|
||||
e => DBError::DBError(Box::new(e)),
|
||||
})?;
|
||||
|
||||
Ok(secret.secret)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use std::env;
|
||||
|
||||
use crate::*;
|
||||
|
||||
use db_core::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn everyting_works() {
|
||||
const EMAIL: &str = "postgresuser@foo.com";
|
||||
const EMAIL2: &str = "postgresuse2r@foo.com";
|
||||
const NAME: &str = "postgresuser";
|
||||
const NAME2: &str = "postgresuser2";
|
||||
const NAME3: &str = "postgresuser3";
|
||||
const NAME4: &str = "postgresuser4";
|
||||
const NAME5: &str = "postgresuser5";
|
||||
const NAME6: &str = "postgresuser6";
|
||||
const NAME7: &str = "postgresuser7";
|
||||
const PASSWORD: &str = "pasdfasdfasdfadf";
|
||||
const SECRET1: &str = "postgressecret1";
|
||||
const SECRET2: &str = "postgressecret2";
|
||||
const SECRET3: &str = "postgressecret3";
|
||||
const SECRET4: &str = "postgressecret4";
|
||||
|
||||
let url = env::var("POSTGRES_DATABASE_URL").unwrap();
|
||||
let pool_options = PgPoolOptions::new().max_connections(2);
|
||||
let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url });
|
||||
let db = connection_options.connect().await.unwrap();
|
||||
|
||||
db.migrate().await.unwrap();
|
||||
email_register_works(&db, EMAIL, NAME, PASSWORD, SECRET1, NAME5).await;
|
||||
username_register_works(&db, NAME2, PASSWORD, SECRET2).await;
|
||||
duplicate_secret_guard_works(&db, NAME3, PASSWORD, NAME4, SECRET3, SECRET2).await;
|
||||
duplicate_username_and_email(&db, NAME6, NAME7, EMAIL2, PASSWORD, SECRET4, NAME, EMAIL).await;
|
||||
let creds = Creds {
|
||||
username: NAME.into(),
|
||||
password: SECRET4.into(),
|
||||
};
|
||||
db.update_password(&creds).await.unwrap();
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "db-sqlx-sqlite"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/realaravinth/gists"
|
||||
repository = "https://github.com/realaravinth/gists"
|
||||
documentation = "https://github.con/realaravinth/gists"
|
||||
readme = "https://github.com/realaravinth/gists/blob/master/README.md"
|
||||
license = "AGPLv3 or later version"
|
||||
authors = ["realaravinth <realaravinth@batsense.net>"]
|
||||
include = ["./mgrations/"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sqlx = { version = "0.5.10", features = [ "sqlite", "time", "offline" ] }
|
||||
db-core = {path = "../db-core"}
|
||||
async-trait = "0.1.51"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2"
|
||||
sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
|
||||
db-core = {path = "../db-core", features = ["test"]}
|
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) UNIQUE DEFAULT NULL,
|
||||
email_verified BOOLEAN DEFAULT NULL,
|
||||
secret varchar(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(150) NOT NULL,
|
||||
ID INTEGER PRIMARY KEY NOT NULL
|
||||
);
|
@ -0,0 +1,169 @@
|
||||
{
|
||||
"db": "SQLite",
|
||||
"08e90ce06e795bdb9bf0ea5b18df3b69b107764ad9d224de601b3588fbeac211": {
|
||||
"query": "UPDATE admin_users set secret = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"09381773667052a357cc51fec91a4004e09ba5ce7ebce2b7378ff637a5cb89a1": {
|
||||
"query": "SELECT id from admin_users WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "ID",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"150c8d182ca90bd50fdd419e5b1b2bb48c8eb5d060d7ab0207dfc04e8eda6fee": {
|
||||
"query": "INSERT INTO admin_users \n (username , password, secret) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"1c6e0ac5913665e512c5d6c8b99af61e6adbac6482de651ff8aeab4210ba4120": {
|
||||
"query": "UPDATE admin_users set email = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"29f35f75a0ddedaabdfecd6a22ee16747f91b2b928641361fe43738eb21e7607": {
|
||||
"query": "DELETE FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"2f9e7d2c4e5335a1c221f8162f5c47c44efbea37cc41670873303f6892e62375": {
|
||||
"query": "UPDATE admin_users set password = $1\n WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"3e91a474c261bae1ca27f1a5fcf9a276b5fa000039f5118b6daf5640b8708894": {
|
||||
"query": "SELECT secret FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "secret",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"4899a9845675082afcfca09d016da313f09932882f5e71ca7da26db94aa14dc1": {
|
||||
"query": "UPDATE admin_users set username = $1 WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"524065157b7c8e0a2ff02eb8efd7ddf79f585b2f1428f9d2bab92b3d2b2b9b71": {
|
||||
"query": "insert into admin_users \n (username , password, email, secret) values ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"677c618882d6b7e621f9f2b040671d8260518383796efad120a8ffdb596e9d7a": {
|
||||
"query": "SELECT username, password FROM admin_users WHERE email = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"c020b4503f7869c5f32cce9a871c99a19e5000bc9aa03ebb0ce8176397367cd3": {
|
||||
"query": "SELECT id from admin_users WHERE username = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "ID",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"dadfd47b8d33b0b636a79716afa5c8afdf9873304ff9fe883de7eae3aa5e8504": {
|
||||
"query": "SELECT password FROM admin_users WHERE username = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use db_core::dev::*;
|
||||
use sqlx::Error;
|
||||
|
||||
pub fn map_register_err(e: Error) -> DBError {
|
||||
if let Error::Database(err) = e {
|
||||
if err.code() == Some(Cow::from("2067")) {
|
||||
let msg = err.message();
|
||||
if msg.contains("admin_users.username") {
|
||||
DBError::DuplicateUsername
|
||||
} else if msg.contains("admin_users.email") {
|
||||
DBError::DuplicateEmail
|
||||
} else if msg.contains("admin_users.secret") {
|
||||
DBError::DuplicateSecret
|
||||
} else {
|
||||
DBError::DBError(Box::new(Error::Database(err)))
|
||||
}
|
||||
} else {
|
||||
DBError::DBError(Box::new(Error::Database(err)))
|
||||
}
|
||||
} else {
|
||||
DBError::DBError(Box::new(e))
|
||||
}
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
use db_core::dev::*;
|
||||
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
|
||||
pub mod errors;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub struct Database {
|
||||
pub pool: SqlitePool,
|
||||
}
|
||||
|
||||
/// Use an existing database pool
|
||||
pub struct Conn(pub SqlitePool);
|
||||
|
||||
/// Connect to databse
|
||||
pub enum ConnectionOptions {
|
||||
/// fresh connection
|
||||
Fresh(Fresh),
|
||||
/// existing connection
|
||||
Existing(Conn),
|
||||
}
|
||||
|
||||
pub struct Fresh {
|
||||
pub pool_options: SqlitePoolOptions,
|
||||
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 GistDatabase for Database {
|
||||
async fn email_login(&self, email: &str) -> DBResult<Creds> {
|
||||
sqlx::query_as!(
|
||||
Creds,
|
||||
r#"SELECT username, password FROM admin_users WHERE email = ($1)"#,
|
||||
email,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::RowNotFound => DBError::AccountNotFound,
|
||||
e => DBError::DBError(Box::new(e)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn username_login(&self, username: &str) -> DBResult<Password> {
|
||||
sqlx::query_as!(
|
||||
Password,
|
||||
r#"SELECT password FROM admin_users WHERE username = ($1)"#,
|
||||
username,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
Error::RowNotFound => DBError::AccountNotFound,
|
||||
e => DBError::DBError(Box::new(e)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn email_register(&self, payload: &EmailRegisterPayload) -> DBResult<()> {
|
||||
sqlx::query!(
|
||||
"insert into admin_users
|
||||
(username , password, email, secret) values ($1, $2, $3, $4)",
|
||||
payload.username,
|
||||
payload.password,
|
||||
payload.email,
|
||||
payload.secret,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn username_register(&self, payload: &UsernameRegisterPayload) -> DBResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO admin_users
|
||||
(username , password, secret) VALUES ($1, $2, $3)",
|
||||
payload.username,
|
||||
payload.password,
|
||||
payload.secret,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_email(&self, payload: &UpdateEmailPayload) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set email = $1
|
||||
WHERE username = $2",
|
||||
payload.email,
|
||||
payload.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
if x.rows_affected() == 0 {
|
||||
return Err(DBError::AccountNotFound);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn update_password(&self, payload: &Creds) -> DBResult<()> {
|
||||
let x = sqlx::query!(
|
||||
"UPDATE admin_users set password = $1
|
||||
WHERE username = $2",
|
||||
payload.password,
|
||||
payload.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError |