use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(clippy::upper_case_acronyms)]
pub enum Action {
Associate,
ChangePublicKeys,
CreateNewGroup,
DatabaseLocked,
DatabaseUnlocked,
GeneratePassword,
GetDatabaseGroups,
GetDatabaseHash,
GetLogins,
GetLoginsCount,
GetTotp,
LockDatabase,
PasskeysGet,
PasskeysRegister,
RequestAutotype,
SetLogin,
TestAssociate,
}
impl Action {
pub const fn as_str(self) -> &'static str {
match self {
Action::Associate => "associate",
Action::ChangePublicKeys => "change-public-keys",
Action::CreateNewGroup => "create-new-group",
Action::DatabaseLocked => "database-locked",
Action::DatabaseUnlocked => "database-unlocked",
Action::GeneratePassword => "generate-password",
Action::GetDatabaseGroups => "get-database-groups",
Action::GetDatabaseHash => "get-databasehash",
Action::GetLogins => "get-logins",
Action::GetLoginsCount => "get-logins-count",
Action::GetTotp => "get-totp",
Action::LockDatabase => "lock-database",
Action::PasskeysGet => "passkeys-get",
Action::PasskeysRegister => "passkeys-register",
Action::RequestAutotype => "request-autotype",
Action::SetLogin => "set-login",
Action::TestAssociate => "test-associate",
}
}
}
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for Action {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"associate" => Action::Associate,
"change-public-keys" => Action::ChangePublicKeys,
"create-new-group" => Action::CreateNewGroup,
"database-locked" => Action::DatabaseLocked,
"database-unlocked" => Action::DatabaseUnlocked,
"generate-password" => Action::GeneratePassword,
"get-database-groups" => Action::GetDatabaseGroups,
"get-databasehash" => Action::GetDatabaseHash,
"get-logins" => Action::GetLogins,
"get-logins-count" => Action::GetLoginsCount,
"get-totp" => Action::GetTotp,
"lock-database" => Action::LockDatabase,
"passkeys-get" => Action::PasskeysGet,
"passkeys-register" => Action::PasskeysRegister,
"request-autotype" => Action::RequestAutotype,
"set-login" => Action::SetLogin,
"test-associate" => Action::TestAssociate,
_ => return Err(()),
})
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Envelope {
pub action: String,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub nonce: Option<String>,
#[serde(default)]
pub client_id: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub public_key: Option<String>,
#[serde(default)]
pub success: Option<SuccessValue>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub error_code: Option<ErrorCode>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum SuccessValue {
Bool(bool),
String(String),
}
impl SuccessValue {
pub fn is_true(&self) -> bool {
match self {
SuccessValue::Bool(value) => *value,
SuccessValue::String(value) => value.eq_ignore_ascii_case("true"),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ErrorCode {
Int(i32),
String(String),
}
impl ErrorCode {
pub fn as_i32(&self) -> Option<i32> {
match self {
ErrorCode::Int(value) => Some(*value),
ErrorCode::String(value) => value.parse().ok(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Association {
pub id: String,
pub key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssociationRecord {
pub association: Association,
pub database_hash: String,
pub version: String,
}
#[derive(Debug, Clone)]
pub struct LoginQuery {
pub url: String,
pub submit_url: Option<String>,
pub http_auth: Option<String>,
pub keys: Vec<Association>,
pub primary_id: Option<String>,
}
impl LoginQuery {
pub fn new(url: impl Into<String>, associations: Vec<Association>) -> Self {
Self {
url: url.into(),
submit_url: None,
http_auth: None,
keys: associations,
primary_id: None,
}
}
pub fn with_submit_url(mut self, submit_url: impl Into<String>) -> Self {
self.submit_url = Some(submit_url.into());
self
}
pub fn with_http_auth(mut self, http_auth: impl Into<String>) -> Self {
self.http_auth = Some(http_auth.into());
self
}
pub fn with_primary_id(mut self, primary_id: impl Into<String>) -> Self {
self.primary_id = Some(primary_id.into());
self
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LoginEntry {
pub login: String,
pub name: String,
pub password: String,
#[serde(default)]
pub uuid: Option<String>,
#[serde(default)]
pub totp: Option<String>,
#[serde(default)]
pub expired: Option<String>,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub string_fields: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GetLoginsResponse {
#[serde(deserialize_with = "deserialize_number_as_string")]
pub count: Option<String>,
pub entries: Option<Vec<LoginEntry>>,
pub nonce: Option<String>,
pub success: Option<SuccessValue>,
pub hash: Option<String>,
pub version: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub id: Option<String>,
}
fn deserialize_number_as_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
use std::fmt;
struct NumberOrString;
impl<'de> Visitor<'de> for NumberOrString {
type Value = Option<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a number or string")
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value.to_string()))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value.to_string()))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value.to_string()))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(value))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
}
deserializer.deserialize_any(NumberOrString)
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseHash {
pub action: Option<String>,
pub hash: Option<String>,
pub version: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GroupNode {
pub name: String,
pub uuid: String,
pub children: Vec<GroupNode>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseGroups {
pub default_group: Option<String>,
pub default_group_always_allow: Option<bool>,
pub groups: Vec<GroupNode>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PasswordResponse {
pub version: Option<String>,
pub password: Option<String>,
pub success: Option<SuccessValue>,
pub nonce: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TotpResponse {
pub version: Option<String>,
pub totp: Option<String>,
pub success: Option<SuccessValue>,
pub nonce: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub error_code: Option<ErrorCode>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TestAssociationResult {
pub version: Option<String>,
pub nonce: Option<String>,
pub hash: Option<String>,
pub id: Option<String>,
pub success: Option<SuccessValue>,
}