This commit is contained in:
Jonas Rabenstein 2026-02-27 03:28:09 +01:00
commit a0d3c6cf9c
8 changed files with 2415 additions and 0 deletions

165
cli/src/authentication.rs Normal file
View file

@ -0,0 +1,165 @@
use clap::{Args, ArgGroup};
use restson::{RestClient, RestPath, Response};
use anyhow::{Result, Error};
use serde::{
ser::{Serialize, Serializer, SerializeMap},
Deserialize,
};
use chrono::{DateTime, Utc};
#[derive(Args, Debug)]
#[command(group(
ArgGroup::new("authentication")
.args(["access", "refresh", "email", "phone"])
))]
pub struct Authentication {
#[arg(long)]
access: Option<Token>,
#[arg(long)]
refresh: Option<Token>,
#[arg(long)]
email: Option<Email>,
#[arg(long)]
phone: Option<Phone>,
}
impl Authentication {
pub async fn apply(self, client: RestClient) -> Result<RestClient> {
let client = match (self.access, self.refresh, self.email, self.phone) {
(Some(v), None, None, None) => v.apply(client)?,
(None, Some(v), None, None) => Tokens::authenticate(client, v).await?,
(None, None, Some(v), None) => Tokens::authenticate(client, v).await?,
(None, None, None, Some(v)) => Tokens::authenticate(client, v).await?,
(None, None, None, None) => client,
(a, b, c, d) => anyhow::bail!("invalid authentication: {} + {} + {} + {}", a.is_some(), b.is_some(), c.is_some(), d.is_some()),
};
Ok(client)
}
}
mod identifier {
#[derive(Debug, Clone)]
pub struct Email;
#[derive(Debug, Clone)]
pub struct Phone;
}
trait Identifier: Clone {
const NAME: &'static str;
type Value: std::str::FromStr<Err=Self::Error> + std::fmt::Debug + Clone + serde::Serialize;
type Error: std::error::Error + Send + Sync + 'static;
}
impl Identifier for identifier::Email {
const NAME: &'static str = "email";
type Value = String;
type Error = <String as std::str::FromStr>::Err;
}
impl Identifier for identifier::Phone {
const NAME: &'static str = "phone";
type Value = String;
type Error = <String as std::str::FromStr>::Err;
}
#[derive(Debug, Clone)]
struct WithPassword<I: Identifier> {
value: I::Value,
password: String,
}
impl<I: Identifier> Serialize for WithPassword<I> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
{
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry(I::NAME, &self.value)?;
map.serialize_entry("password", &self.password)?;
map.end()
}
}
impl<I: Identifier> RestPath<()> for WithPassword<I> {
fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
Ok(String::from("auth2/login"))
}
}
type Email = WithPassword<identifier::Email>;
type Phone = WithPassword<identifier::Phone>;
impl<I: Identifier> std::str::FromStr for WithPassword<I> {
type Err= Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let password = match std::env::var("SPOND_PASSWORD") {
Ok(password) => password,
Err(_) => rpassword::prompt_password("Password: ")?,
};
let value = I::Value::from_str(s)?;
Ok(Self { value, password })
}
}
#[derive(Debug, Deserialize)]
struct Tokens {
#[serde(rename = "accessToken")]
access: TokenWithExpiration,
#[serde(rename = "refreshToken")]
refresh: TokenWithExpiration,
}
impl Tokens {
async fn authenticate<R: serde::Serialize + RestPath<()>>(client: RestClient, request: R) -> Result<RestClient> {
let tokens: Response<Self> = client.post_capture((), &request).await?;
tokens.into_inner().apply(client)
}
fn apply(self, client: RestClient) -> Result<RestClient> {
println!("refresh: {self:?}");
self.access.token.apply(client)
}
}
#[derive(Debug, Deserialize)]
struct TokenWithExpiration {
token: Token,
#[allow(unused)]
expiration: DateTime<Utc>
}
#[derive(Debug, Clone, Deserialize)]
struct Token(String);
impl Serialize for Token {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
{
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("token", &self.0)?;
map.end()
}
}
impl Token {
fn apply(self, mut client: RestClient) -> Result<RestClient> {
client.set_header("Authorization", &format!("Bearer {}", self.0))?;
Ok(client)
}
}
impl RestPath<()> for Token {
fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
Ok(String::from("auth2/login/refresh"))
}
}
impl std::str::FromStr for Token {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_string()))
}
}