use cratefield_core::{Database, DbError, Row, Statement};
use sea_query::{Alias, Condition, Expr, Query};
const TABLE: &str = "device_auth_codes";
fn iden(name: &str) -> Alias {
Alias::new(name)
}
pub(crate) struct CodeRow {
pub(crate) client_id: String,
pub(crate) scopes: String,
pub(crate) name: Option<String>,
pub(crate) status: String,
pub(crate) approver_subject: Option<String>,
pub(crate) expires_at: String,
pub(crate) interval_secs: i64,
}
fn row_from(row: &Row) -> CodeRow {
CodeRow {
client_id: row.get("client_id").unwrap_or_default(),
scopes: row.get("scopes").unwrap_or_default(),
name: row.get("name"),
status: row.get("status").unwrap_or_default(),
approver_subject: row.get("approver_subject"),
expires_at: row.get("expires_at").unwrap_or_default(),
interval_secs: row.get("interval_secs").unwrap_or(0),
}
}
pub(crate) struct NewCode<'a> {
pub(crate) device_code_hash: &'a str,
pub(crate) user_code_hash: &'a str,
pub(crate) client_id: &'a str,
pub(crate) scopes: &'a str,
pub(crate) name: Option<&'a str>,
pub(crate) now: &'a str,
pub(crate) expires_at: &'a str,
pub(crate) interval_secs: i64,
}
pub(crate) async fn insert(db: &dyn Database, code: &NewCode<'_>) -> Result<u64, DbError> {
let mut insert = Query::insert();
insert
.into_table(iden(TABLE))
.columns([
"device_code_hash",
"user_code_hash",
"client_id",
"scopes",
"name",
"status",
"created_at",
"expires_at",
"interval_secs",
])
.values_panic([
code.device_code_hash.into(),
code.user_code_hash.into(),
code.client_id.into(),
code.scopes.into(),
code.name.into(),
"pending".into(),
code.now.into(),
code.expires_at.into(),
code.interval_secs.into(),
]);
db.execute(&Statement::render(&insert)).await
}
pub(crate) async fn user_code_exists(
db: &dyn Database,
user_code_hash: &str,
) -> Result<bool, DbError> {
let mut select = Query::select();
select
.column(iden("user_code_hash"))
.from(iden(TABLE))
.and_where(Expr::col(iden("user_code_hash")).eq(user_code_hash));
Ok(!db.query(&Statement::render(&select)).await?.is_empty())
}
pub(crate) async fn find_by_device(
db: &dyn Database,
device_code_hash: &str,
client_id: &str,
) -> Result<Option<CodeRow>, DbError> {
let mut select = Query::select();
select
.columns([
"client_id",
"scopes",
"name",
"status",
"approver_subject",
"expires_at",
"interval_secs",
])
.from(iden(TABLE))
.and_where(Expr::col(iden("device_code_hash")).eq(device_code_hash))
.and_where(Expr::col(iden("client_id")).eq(client_id));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(row_from))
}
pub(crate) async fn find_pending_by_user(
db: &dyn Database,
user_code_hash: &str,
now: &str,
) -> Result<Option<CodeRow>, DbError> {
let mut select = Query::select();
select
.columns([
"client_id",
"scopes",
"name",
"status",
"approver_subject",
"expires_at",
"interval_secs",
])
.from(iden(TABLE))
.and_where(Expr::col(iden("user_code_hash")).eq(user_code_hash))
.and_where(Expr::col(iden("status")).eq("pending"))
.and_where(Expr::col(iden("expires_at")).gt(now));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(row_from))
}
pub(crate) async fn touch_poll(
db: &dyn Database,
device_code_hash: &str,
client_id: &str,
now: &str,
not_after: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(TABLE))
.value(iden("last_polled_at"), now)
.and_where(Expr::col(iden("device_code_hash")).eq(device_code_hash))
.and_where(Expr::col(iden("client_id")).eq(client_id))
.and_where(Expr::col(iden("status")).eq("pending"))
.and_where(Expr::col(iden("expires_at")).gt(now))
.and_where(
Condition::any()
.add(Expr::col(iden("last_polled_at")).is_null())
.add(Expr::col(iden("last_polled_at")).lte(not_after))
.into(),
);
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn widen_interval(
db: &dyn Database,
device_code_hash: &str,
client_id: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(TABLE))
.value(
iden("interval_secs"),
Expr::col(iden("interval_secs")).add(5),
)
.and_where(Expr::col(iden("device_code_hash")).eq(device_code_hash))
.and_where(Expr::col(iden("client_id")).eq(client_id));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn consume(
db: &dyn Database,
device_code_hash: &str,
client_id: &str,
now: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(TABLE))
.value(iden("status"), "consumed")
.and_where(Expr::col(iden("device_code_hash")).eq(device_code_hash))
.and_where(Expr::col(iden("client_id")).eq(client_id))
.and_where(Expr::col(iden("status")).eq("approved"))
.and_where(Expr::col(iden("expires_at")).gt(now));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn decide(
db: &dyn Database,
user_code_hash: &str,
status: &str,
approver_subject: &str,
now: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(TABLE))
.values([
(iden("status"), status.into()),
(iden("approver_subject"), approver_subject.into()),
])
.and_where(Expr::col(iden("user_code_hash")).eq(user_code_hash))
.and_where(Expr::col(iden("status")).eq("pending"))
.and_where(Expr::col(iden("expires_at")).gt(now));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn purge_expired(db: &dyn Database, now: &str) -> Result<u64, DbError> {
let mut delete = Query::delete();
delete
.from_table(iden(TABLE))
.and_where(Expr::col(iden("expires_at")).lte(now));
db.execute(&Statement::render(&delete)).await
}