use std::error::Error;
use thiserror::Error;
use dotenvy::dotenv;
use sqlx::postgres::PgPoolOptions;
pub use sqlx::PgPool;
pub mod auth;
mod types;
pub use types::*;
mod counter_data;
pub use counter_data::*;
pub mod counter;
pub mod phase;
pub type PgTx = sqlx::Transaction<'static, sqlx::Postgres>;
#[derive(Debug, thiserror::Error)]
pub enum BackendError {
#[error("Invalid Token")]
InvalidToken,
#[error("Database Error: {0}")]
DatabaseError(#[from] sqlx::error::Error),
#[error("Counter data not found")]
CounterNotFound,
#[error("Not authorized to access data")]
Unauthorized,
#[error("User data not found")]
UserNotFound,
#[error("Internal Server Error\nGot Error: {0}")]
Internal(String),
#[error("Username already exists")]
UserExists,
#[error("Invalid Username or Password")]
InvalidSecrets,
#[error("Invalid Password provided")]
InvalidPassword,
#[error("Invalid Username provided")]
InvalidUsername,
#[error("Could not find {0} data for user")]
DataNotFound(String),
}
pub trait DatabaseError: Error {}
#[derive(Debug, thiserror::Error)]
pub enum SignupError {
#[error("Username already exists")]
UserExists,
#[error("Internal Server error when creating user\nGot Error: {0}")]
Internal(String),
#[error("Failed to generate token for new user")]
GenerateToken,
}
impl DatabaseError for SignupError {}
#[derive(Debug, thiserror::Error)]
pub enum ChangeUserError {
#[error("Username already exists")]
UserExists,
#[error("User provided the wrong password")]
InvalidPassword(#[from] AuthorizationError),
#[error("Internal Server error when modifying user")]
Internal(#[from] sqlx::Error),
}
impl DatabaseError for ChangeUserError {}
#[derive(Debug, Error)]
pub enum LoginError {
#[error("Account does not exist")]
InvalidUsername,
#[error("User provided the wrong password")]
InvalidPassword,
#[error("Provided username or password was incorrect")]
InvalidSecrets,
#[error("Internal Server error when logging in user\nGot Error: {0}")]
Internal(String),
}
impl DatabaseError for LoginError {}
#[derive(Debug, Error)]
pub enum AuthorizationError {
#[error("Provided Token is expired")]
ExpiredToken,
#[error("Provided Token is Invalid")]
InvalidToken,
#[error("Provided Username does not exist")]
UserNotFound,
#[error("Provided Password is incorrect")]
InvalidPassword,
#[error("Internal Server error when checking AuthToken\nGot Error: {0}")]
Internal(String),
#[error("Provide Username is Invalid")]
InvalidUsername,
}
impl DatabaseError for AuthorizationError {}
pub async fn create_pool() -> Result<PgPool, sqlx::error::Error> {
dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await?;
Ok(pool)
}
pub async fn get_counter_by_id(
pool: &PgPool,
username: &str,
token: uuid::Uuid,
uuid: uuid::Uuid,
) -> Result<DbCounter, BackendError> {
let user = auth::get_user(pool, username, token).await?;
let counter = match sqlx::query_as!(
DbCounter,
r#"
SELECT * FROM counters
WHERE uuid = $1 AND owner_uuid = $2
"#,
uuid,
user.uuid,
)
.fetch_one(pool)
.await
{
Ok(counter) => counter,
Err(sqlx::Error::RowNotFound) => Err(BackendError::CounterNotFound)?,
Err(err) => Err(err)?,
};
if counter.owner_uuid != user.uuid {
Err(BackendError::Unauthorized)?;
}
Ok(counter)
}
pub async fn get_phase_by_id(
pool: &PgPool,
username: &str,
token: uuid::Uuid,
phase_id: uuid::Uuid,
) -> Result<DbPhase, BackendError> {
let user = auth::get_user(pool, username, token).await?;
let phase: DbPhase = sqlx::query_as("SELECT * FROM phases WHERE uuid = $1 AND owner_uuid = $2")
.bind(phase_id)
.bind(user.uuid)
.fetch_one(pool)
.await?;
Ok(phase)
}
pub async fn update_phase(
pool: &PgPool,
username: &str,
token: uuid::Uuid,
phase: DbPhase,
) -> Result<(), BackendError> {
let _ = auth::get_user(pool, username, token).await?;
let _ = sqlx::query(
r#"
INSERT INTO phases (uuid, owner_uuid, parent_uuid, name, count, time, hunt_type, has_charm, success)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (uuid) DO UPDATE
SET name = $4,
count = $5,
time = $6,
hunt_type = $7,
has_charm = $8,
success = $9
"#,
)
.bind(phase.uuid)
.bind(phase.owner_uuid)
.bind(phase.parent_uuid)
.bind(phase.name)
.bind(phase.count)
.bind(phase.time)
.bind(phase.hunt_type)
.bind(phase.has_charm)
.bind(phase.success)
.execute(pool)
.await?;
Ok(())
}
pub async fn update_counter(
pool: &PgPool,
username: &str,
token: uuid::Uuid,
counter: DbCounter,
) -> Result<(), BackendError> {
let _ = auth::get_user(pool, username, token).await?;
sqlx::query!(
r#"
INSERT INTO counters (uuid, owner_uuid, name)
VALUES ($1, $2, $3)
ON CONFLICT (uuid) DO UPDATE
SET name = $3
"#,
counter.uuid,
counter.owner_uuid,
counter.name,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn remove_counter(
pool: &PgPool,
username: &str,
token: uuid::Uuid,
counter_uuid: uuid::Uuid,
) -> Result<(), BackendError> {
let user = auth::get_user(pool, username, token).await?;
sqlx::query!(
r#"
delete from counters
where owner_uuid = $1 AND uuid = $2
"#,
user.uuid,
counter_uuid,
)
.execute(pool)
.await?;
sqlx::query!(
r#"
DELETE FROM phases
WHERE owner_uuid = $1 AND parent_uuid = $2
"#,
user.uuid,
counter_uuid,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn remove_phase(pool: &PgPool, phase_id: uuid::Uuid) -> Result<(), sqlx::error::Error> {
sqlx::query!(
r#"
delete from phases
where uuid = $1
"#,
phase_id
)
.execute(pool)
.await?;
Ok(())
}