summaryrefslogtreecommitdiff
path: root/src/services/db/client.rs
blob: c25ad0d4b43b8735c7625c10fa433c9c381ef61d (plain)
use std::str::FromStr;

use exun::{RawUnexpected, ResultErrorExt};
use sqlx::{
	mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, FromRow, MySql, Transaction,
};
use url::Url;
use uuid::Uuid;

use crate::{
	models::client::{Client, ClientType},
	services::crypto::PasswordHash,
};

#[derive(Debug, Clone, FromRow)]
pub struct ClientRow {
	pub id: Uuid,
	pub alias: String,
	pub client_type: ClientType,
	pub allowed_scopes: String,
	pub default_scopes: Option<String>,
}

pub async fn client_id_exists<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<bool, RawUnexpected> {
	query_scalar!(
		r"SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`",
		id
	)
	.fetch_one(executor)
	.await
	.unexpect()
}

pub async fn client_alias_exists<'c>(
	executor: impl Executor<'c, Database = MySql>,
	alias: &str,
) -> Result<bool, RawUnexpected> {
	query_scalar!(
		"SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`",
		alias
	)
	.fetch_one(executor)
	.await
	.unexpect()
}

pub async fn get_client_response<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Option<ClientRow>, RawUnexpected> {
	let record = query_as!(
		ClientRow,
		r"SELECT id as `id: Uuid`,
		         alias,
				 type as `client_type: ClientType`,
				 allowed_scopes,
				 default_scopes
		  FROM clients WHERE id = ?",
		id
	)
	.fetch_optional(executor)
	.await?;

	Ok(record)
}

pub async fn get_client_alias<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Option<Box<str>>, RawUnexpected> {
	let alias = query_scalar!("SELECT alias FROM clients WHERE id = ?", id)
		.fetch_optional(executor)
		.await
		.unexpect()?;

	Ok(alias.map(String::into_boxed_str))
}

pub async fn get_client_type<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Option<ClientType>, RawUnexpected> {
	let ty = query_scalar!(
		"SELECT type as `type: ClientType` FROM clients WHERE id = ?",
		id
	)
	.fetch_optional(executor)
	.await
	.unexpect()?;

	Ok(ty)
}

pub async fn get_client_allowed_scopes<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Option<Box<str>>, RawUnexpected> {
	let scopes = query_scalar!("SELECT allowed_scopes FROM clients WHERE id = ?", id)
		.fetch_optional(executor)
		.await?;

	Ok(scopes.map(Box::from))
}

pub async fn get_client_default_scopes<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Option<Option<Box<str>>>, RawUnexpected> {
	let scopes = query_scalar!("SELECT default_scopes FROM clients WHERE id = ?", id)
		.fetch_optional(executor)
		.await?;

	Ok(scopes.map(|s| s.map(Box::from)))
}

pub async fn get_client_redirect_uris<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<Box<[Url]>, RawUnexpected> {
	let uris = query_scalar!(
		"SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?",
		id
	)
	.fetch_all(executor)
	.await
	.unexpect()?;

	uris.into_iter()
		.map(|s| Url::from_str(&s).unexpect())
		.collect()
}

pub async fn client_has_redirect_uri<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	url: Url,
) -> Result<bool, RawUnexpected> {
	query_scalar!(
		r"SELECT EXISTS(
			  SELECT redirect_uri
			  FROM client_redirect_uris
			  WHERE client_id = ? AND redirect_uri = ?
		  ) as `e: bool`",
		id,
		url.to_string()
	)
	.fetch_one(executor)
	.await
	.unexpect()
}

async fn delete_client_redirect_uris<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
) -> Result<(), sqlx::Error> {
	query!("DELETE FROM client_redirect_uris WHERE client_id = ?", id)
		.execute(executor)
		.await?;
	Ok(())
}

async fn create_client_redirect_uris<'c>(
	mut transaction: Transaction<'c, MySql>,
	client_id: Uuid,
	uris: &[Url],
) -> Result<(), sqlx::Error> {
	for uri in uris {
		query!(
			r"INSERT INTO client_redirect_uris (client_id, redirect_uri)
									    VALUES (        ?,            ?)",
			client_id,
			uri.to_string()
		)
		.execute(&mut transaction)
		.await?;
	}

	transaction.commit().await?;

	Ok(())
}

pub async fn create_client<'c>(
	mut transaction: Transaction<'c, MySql>,
	client: &Client,
) -> Result<(), sqlx::Error> {
	query!(
		r"INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes)
					   VALUES ( ?,     ?,    ?,           ?,           ?,              ?,              ?,              ?)",
		client.id(),
		client.alias(),
		client.client_type(),
		client.secret_hash(),
		client.secret_salt(),
		client.secret_version(),
		client.allowed_scopes(),
		client.default_scopes()
	)
	.execute(&mut transaction)
	.await?;

	create_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?;

	Ok(())
}

pub async fn update_client<'c>(
	mut transaction: Transaction<'c, MySql>,
	client: &Client,
) -> Result<(), sqlx::Error> {
	query!(
		r"UPDATE clients SET
		alias = ?,
		type = ?,
		secret_hash = ?,
		secret_salt = ?,
		secret_version = ?,
		allowed_scopes = ?,
		default_scopes = ?
		WHERE id = ?",
		client.client_type(),
		client.alias(),
		client.secret_hash(),
		client.secret_salt(),
		client.secret_version(),
		client.allowed_scopes(),
		client.default_scopes(),
		client.id()
	)
	.execute(&mut transaction)
	.await?;

	update_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?;

	Ok(())
}

pub async fn update_client_alias<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	alias: &str,
) -> Result<MySqlQueryResult, sqlx::Error> {
	query!("UPDATE clients SET alias = ? WHERE id = ?", alias, id)
		.execute(executor)
		.await
}

pub async fn update_client_type<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	ty: ClientType,
) -> Result<MySqlQueryResult, sqlx::Error> {
	query!("UPDATE clients SET type = ? WHERE id = ?", ty, id)
		.execute(executor)
		.await
}

pub async fn update_client_allowed_scopes<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	allowed_scopes: &str,
) -> Result<MySqlQueryResult, sqlx::Error> {
	query!(
		"UPDATE clients SET allowed_scopes = ? WHERE id = ?",
		allowed_scopes,
		id
	)
	.execute(executor)
	.await
}

pub async fn update_client_default_scopes<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	default_scopes: Option<String>,
) -> Result<MySqlQueryResult, sqlx::Error> {
	query!(
		"UPDATE clients SET default_scopes = ? WHERE id = ?",
		default_scopes,
		id
	)
	.execute(executor)
	.await
}

pub async fn update_client_redirect_uris<'c>(
	mut transaction: Transaction<'c, MySql>,
	id: Uuid,
	uris: &[Url],
) -> Result<(), sqlx::Error> {
	delete_client_redirect_uris(&mut transaction, id).await?;
	create_client_redirect_uris(transaction, id, uris).await?;
	Ok(())
}

pub async fn update_client_secret<'c>(
	executor: impl Executor<'c, Database = MySql>,
	id: Uuid,
	secret: Option<PasswordHash>,
) -> Result<MySqlQueryResult, sqlx::Error> {
	if let Some(secret) = secret {
		query!(
			"UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?",
			secret.hash(),
			secret.salt(),
			secret.version(),
			id
		)
		.execute(executor)
		.await
	} else {
		query!(
			r"UPDATE clients
			  SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL
			  WHERE id = ?",
			id
		)
		.execute(executor)
		.await
	}
}