feat: add password change and key rotation endpoints for user accounts
This commit is contained in:
parent
6f842cc2ce
commit
9a028169b7
4 changed files with 367 additions and 44 deletions
|
|
@ -1,17 +1,23 @@
|
||||||
use axum::{extract::State, Json};
|
use axum::{extract::State, Json};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use constant_time_eq::constant_time_eq;
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use worker::{query, Env};
|
use worker::{query, D1PreparedStatement, Env};
|
||||||
|
|
||||||
|
use super::get_batch_size;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::Claims,
|
auth::Claims,
|
||||||
crypto::{generate_salt, hash_password_for_storage, verify_password},
|
crypto::{generate_salt, hash_password_for_storage},
|
||||||
db,
|
db,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
models::user::{DeleteAccountRequest, PreloginResponse, RegisterRequest, User},
|
models::{
|
||||||
|
cipher::CipherData,
|
||||||
|
user::{
|
||||||
|
ChangePasswordRequest, DeleteAccountRequest, PreloginResponse, RegisterRequest,
|
||||||
|
RotateKeyRequest, User,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[worker::send]
|
#[worker::send]
|
||||||
|
|
@ -119,22 +125,22 @@ pub async fn revision_date(
|
||||||
claims: Claims,
|
claims: Claims,
|
||||||
State(env): State<Arc<Env>>,
|
State(env): State<Arc<Env>>,
|
||||||
) -> Result<Json<i64>, AppError> {
|
) -> Result<Json<i64>, AppError> {
|
||||||
let db = db::get_db(&env)? ;
|
let db = db::get_db(&env)?;
|
||||||
|
|
||||||
// get the user's updated_at timestamp
|
// get the user's updated_at timestamp
|
||||||
let updated_at: Option<String> = db
|
let updated_at: Option<String> = db
|
||||||
.prepare("SELECT updated_at FROM users WHERE id = ?1")
|
.prepare("SELECT updated_at FROM users WHERE id = ?1")
|
||||||
.bind(&[claims.sub. into()])?
|
.bind(&[claims.sub.into()])?
|
||||||
.first(Some("updated_at"))
|
.first(Some("updated_at"))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AppError::Database)?;
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
// convert the timestamp to a millisecond-level Unix timestamp
|
// convert the timestamp to a millisecond-level Unix timestamp
|
||||||
let revision_date = updated_at
|
let revision_date = updated_at
|
||||||
.and_then(|ts| chrono::DateTime::parse_from_rfc3339(&ts). ok())
|
.and_then(|ts| chrono::DateTime::parse_from_rfc3339(&ts).ok())
|
||||||
. map(|dt| dt.timestamp_millis())
|
.map(|dt| dt.timestamp_millis())
|
||||||
.unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
|
.unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
|
||||||
|
|
||||||
Ok(Json(revision_date))
|
Ok(Json(revision_date))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,18 +168,9 @@ pub async fn delete_account(
|
||||||
.master_password_hash
|
.master_password_hash
|
||||||
.ok_or_else(|| AppError::BadRequest("Missing master password hash".to_string()))?;
|
.ok_or_else(|| AppError::BadRequest("Missing master password hash".to_string()))?;
|
||||||
|
|
||||||
let is_valid = if let Some(ref salt) = user.password_salt {
|
let verification = user.verify_master_password(&provided_hash).await?;
|
||||||
// New user with PBKDF2 hashed password
|
|
||||||
verify_password(&provided_hash, &user.master_password_hash, salt).await?
|
|
||||||
} else {
|
|
||||||
// Legacy user: direct comparison (migration happens at login, not delete)
|
|
||||||
constant_time_eq(
|
|
||||||
user.master_password_hash.as_bytes(),
|
|
||||||
provided_hash.as_bytes(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid {
|
if !verification.is_valid() {
|
||||||
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,3 +194,183 @@ pub async fn delete_account(
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST /accounts/password - Change master password
|
||||||
|
#[worker::send]
|
||||||
|
pub async fn post_password(
|
||||||
|
claims: Claims,
|
||||||
|
State(env): State<Arc<Env>>,
|
||||||
|
Json(payload): Json<ChangePasswordRequest>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let db = db::get_db(&env)?;
|
||||||
|
let user_id = &claims.sub;
|
||||||
|
|
||||||
|
// Get the user from the database
|
||||||
|
let user: Value = db
|
||||||
|
.prepare("SELECT * FROM users WHERE id = ?1")
|
||||||
|
.bind(&[user_id.clone().into()])?
|
||||||
|
.first(None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
||||||
|
let user: User = serde_json::from_value(user).map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// Verify the current master password
|
||||||
|
let verification = user
|
||||||
|
.verify_master_password(&payload.master_password_hash)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !verification.is_valid() {
|
||||||
|
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new salt and hash the new password
|
||||||
|
let new_salt = generate_salt()?;
|
||||||
|
let new_hashed_password =
|
||||||
|
hash_password_for_storage(&payload.new_master_password_hash, &new_salt).await?;
|
||||||
|
|
||||||
|
// Generate new security stamp and update timestamp
|
||||||
|
let new_security_stamp = Uuid::new_v4().to_string();
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
// Update user record
|
||||||
|
query!(
|
||||||
|
&db,
|
||||||
|
"UPDATE users SET master_password_hash = ?1, password_salt = ?2, key = ?3, master_password_hint = ?4, security_stamp = ?5, updated_at = ?6 WHERE id = ?7",
|
||||||
|
new_hashed_password,
|
||||||
|
new_salt,
|
||||||
|
payload.key,
|
||||||
|
payload.master_password_hint,
|
||||||
|
new_security_stamp,
|
||||||
|
now,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(json!({})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /accounts/key-management/rotate-user-account-keys - Rotate user encryption keys
|
||||||
|
#[worker::send]
|
||||||
|
pub async fn post_rotatekey(
|
||||||
|
claims: Claims,
|
||||||
|
State(env): State<Arc<Env>>,
|
||||||
|
Json(payload): Json<RotateKeyRequest>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let db = db::get_db(&env)?;
|
||||||
|
let user_id = &claims.sub;
|
||||||
|
let batch_size = get_batch_size(&env);
|
||||||
|
|
||||||
|
// Get the user from the database
|
||||||
|
let user: Value = db
|
||||||
|
.prepare("SELECT * FROM users WHERE id = ?1")
|
||||||
|
.bind(&[user_id.clone().into()])?
|
||||||
|
.first(None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
||||||
|
let user: User = serde_json::from_value(user).map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// Verify the current master password
|
||||||
|
let verification = user
|
||||||
|
.verify_master_password(&payload.old_master_key_authentication_hash)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !verification.is_valid() {
|
||||||
|
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that email and kdf settings match
|
||||||
|
let unlock_data = &payload.account_unlock_data.master_password_unlock_data;
|
||||||
|
if user.email != unlock_data.email {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Email mismatch in rotation request".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||||
|
|
||||||
|
// Update all folders with new encrypted names (batch operation)
|
||||||
|
let mut folder_statements: Vec<D1PreparedStatement> =
|
||||||
|
Vec::with_capacity(payload.account_data.folders.len());
|
||||||
|
for folder in &payload.account_data.folders {
|
||||||
|
let stmt = query!(
|
||||||
|
&db,
|
||||||
|
"UPDATE folders SET name = ?1, updated_at = ?2 WHERE id = ?3 AND user_id = ?4",
|
||||||
|
folder.name,
|
||||||
|
now,
|
||||||
|
folder.id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
folder_statements.push(stmt);
|
||||||
|
}
|
||||||
|
db::execute_in_batches(&db, folder_statements, batch_size).await?;
|
||||||
|
|
||||||
|
// Update all ciphers with new encrypted data (batch operation)
|
||||||
|
// Only update personal ciphers (organization_id is None)
|
||||||
|
let mut cipher_statements: Vec<D1PreparedStatement> =
|
||||||
|
Vec::with_capacity(payload.account_data.ciphers.len());
|
||||||
|
for cipher in &payload.account_data.ciphers {
|
||||||
|
// Skip organization ciphers
|
||||||
|
if cipher.organization_id.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cipher_data = CipherData {
|
||||||
|
name: cipher.name.clone(),
|
||||||
|
notes: cipher.notes.clone(),
|
||||||
|
login: cipher.login.clone(),
|
||||||
|
card: cipher.card.clone(),
|
||||||
|
identity: cipher.identity.clone(),
|
||||||
|
secure_note: cipher.secure_note.clone(),
|
||||||
|
fields: cipher.fields.clone(),
|
||||||
|
password_history: cipher.password_history.clone(),
|
||||||
|
reprompt: cipher.reprompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = serde_json::to_string(&cipher_data).map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
let stmt = query!(
|
||||||
|
&db,
|
||||||
|
"UPDATE ciphers SET data = ?1, folder_id = ?2, favorite = ?3, updated_at = ?4 WHERE id = ?5 AND user_id = ?6",
|
||||||
|
data,
|
||||||
|
cipher.folder_id,
|
||||||
|
cipher.favorite,
|
||||||
|
now,
|
||||||
|
cipher.id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
cipher_statements.push(stmt);
|
||||||
|
}
|
||||||
|
db::execute_in_batches(&db, cipher_statements, batch_size).await?;
|
||||||
|
|
||||||
|
// Generate new salt and hash the new password
|
||||||
|
let new_salt = generate_salt()?;
|
||||||
|
let new_hashed_password =
|
||||||
|
hash_password_for_storage(&unlock_data.master_key_authentication_hash, &new_salt).await?;
|
||||||
|
|
||||||
|
// Generate new security stamp
|
||||||
|
let new_security_stamp = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Update user record with new keys and password
|
||||||
|
query!(
|
||||||
|
&db,
|
||||||
|
"UPDATE users SET master_password_hash = ?1, password_salt = ?2, key = ?3, private_key = ?4, security_stamp = ?5, updated_at = ?6 WHERE id = ?7",
|
||||||
|
new_hashed_password,
|
||||||
|
new_salt,
|
||||||
|
unlock_data.master_key_encrypted_user_key,
|
||||||
|
payload.account_keys.user_key_encrypted_account_private_key,
|
||||||
|
new_security_stamp,
|
||||||
|
now,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(json!({})))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use axum::{extract::State, Form, Json};
|
use axum::{extract::State, Form, Json};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use constant_time_eq::constant_time_eq;
|
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
@ -9,7 +8,7 @@ use worker::{query, Env};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::Claims,
|
auth::Claims,
|
||||||
crypto::{generate_salt, hash_password_for_storage, verify_password},
|
crypto::{generate_salt, hash_password_for_storage},
|
||||||
db,
|
db,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
models::user::User,
|
models::user::User,
|
||||||
|
|
@ -139,27 +138,16 @@ pub async fn token(
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AppError::Unauthorized("Invalid credentials".to_string()))?
|
.map_err(|_| AppError::Unauthorized("Invalid credentials".to_string()))?
|
||||||
.ok_or_else(|| AppError::Unauthorized("Invalid credentials".to_string()))?;
|
.ok_or_else(|| AppError::Unauthorized("Invalid credentials".to_string()))?;
|
||||||
let user: User =
|
let user: User = serde_json::from_value(user_value).map_err(|_| AppError::Internal)?;
|
||||||
serde_json::from_value(user_value).map_err(|_| AppError::Internal)?;
|
|
||||||
|
|
||||||
// Check if user needs migration (no salt = legacy user)
|
let verification = user.verify_master_password(&password_hash).await?;
|
||||||
let is_valid = if let Some(ref salt) = user.password_salt {
|
|
||||||
// New user with PBKDF2 hashed password
|
|
||||||
verify_password(&password_hash, &user.master_password_hash, salt).await?
|
|
||||||
} else {
|
|
||||||
// Legacy user: direct comparison
|
|
||||||
constant_time_eq(
|
|
||||||
user.master_password_hash.as_bytes(),
|
|
||||||
password_hash.as_bytes(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid {
|
if !verification.is_valid() {
|
||||||
return Err(AppError::Unauthorized("Invalid credentials".to_string()));
|
return Err(AppError::Unauthorized("Invalid credentials".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate legacy user to PBKDF2 if password matches and no salt exists
|
// Migrate legacy user to PBKDF2 if password matches and no salt exists
|
||||||
let user = if user.password_salt.is_none() {
|
let user = if verification.needs_migration() {
|
||||||
// Generate new salt and hash the password
|
// Generate new salt and hash the password
|
||||||
let new_salt = generate_salt()?;
|
let new_salt = generate_salt()?;
|
||||||
let new_hash = hash_password_for_storage(&password_hash, &new_salt).await?;
|
let new_hash = hash_password_for_storage(&password_hash, &new_salt).await?;
|
||||||
|
|
@ -219,4 +207,4 @@ pub async fn token(
|
||||||
}
|
}
|
||||||
_ => Err(AppError::BadRequest("Unsupported grant_type".to_string())),
|
_ => Err(AppError::BadRequest("Unsupported grant_type".to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
use constant_time_eq::constant_time_eq;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{crypto::verify_password, error::AppError};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -20,6 +23,53 @@ pub struct User {
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PasswordVerification {
|
||||||
|
MatchCurrentScheme,
|
||||||
|
MatchLegacyScheme,
|
||||||
|
Mismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordVerification {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
PasswordVerification::MatchCurrentScheme | PasswordVerification::MatchLegacyScheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_migration(&self) -> bool {
|
||||||
|
matches!(self, PasswordVerification::MatchLegacyScheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn verify_master_password(
|
||||||
|
&self,
|
||||||
|
provided_hash: &str,
|
||||||
|
) -> Result<PasswordVerification, AppError> {
|
||||||
|
if let Some(ref salt) = self.password_salt {
|
||||||
|
let is_valid = verify_password(provided_hash, &self.master_password_hash, salt).await?;
|
||||||
|
Ok(if is_valid {
|
||||||
|
PasswordVerification::MatchCurrentScheme
|
||||||
|
} else {
|
||||||
|
PasswordVerification::Mismatch
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let is_valid = constant_time_eq(
|
||||||
|
self.master_password_hash.as_bytes(),
|
||||||
|
provided_hash.as_bytes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(if is_valid {
|
||||||
|
PasswordVerification::MatchLegacyScheme
|
||||||
|
} else {
|
||||||
|
PasswordVerification::Mismatch
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod bool_from_int {
|
mod bool_from_int {
|
||||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
|
@ -83,4 +133,96 @@ pub struct DeleteAccountRequest {
|
||||||
#[serde(alias = "MasterPasswordHash")]
|
#[serde(alias = "MasterPasswordHash")]
|
||||||
pub master_password_hash: Option<String>,
|
pub master_password_hash: Option<String>,
|
||||||
pub otp: Option<String>,
|
pub otp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For POST /accounts/password request
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChangePasswordRequest {
|
||||||
|
pub master_password_hash: String,
|
||||||
|
pub new_master_password_hash: String,
|
||||||
|
pub master_password_hint: Option<String>,
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For POST /accounts/key-management/rotate-user-account-keys request
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateKeyRequest {
|
||||||
|
pub account_unlock_data: RotateAccountUnlockData,
|
||||||
|
pub account_keys: RotateAccountKeys,
|
||||||
|
pub account_data: RotateAccountData,
|
||||||
|
pub old_master_key_authentication_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateAccountUnlockData {
|
||||||
|
pub master_password_unlock_data: MasterPasswordUnlockData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MasterPasswordUnlockData {
|
||||||
|
pub kdf_type: i32,
|
||||||
|
pub kdf_iterations: i32,
|
||||||
|
pub kdf_parallelism: Option<i32>,
|
||||||
|
pub kdf_memory: Option<i32>,
|
||||||
|
pub email: String,
|
||||||
|
pub master_key_authentication_hash: String,
|
||||||
|
pub master_key_encrypted_user_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateAccountKeys {
|
||||||
|
pub user_key_encrypted_account_private_key: String,
|
||||||
|
pub account_public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateAccountData {
|
||||||
|
pub ciphers: Vec<RotateCipherData>,
|
||||||
|
pub folders: Vec<RotateFolderData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateCipherData {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub r#type: i32,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub folder_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub organization_id: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub notes: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub favorite: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub login: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub card: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub identity: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub secure_note: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fields: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password_history: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reprompt: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RotateFolderData {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ pub fn api_router(env: Env) -> Router {
|
||||||
// Delete account
|
// Delete account
|
||||||
.route("/api/accounts", delete(accounts::delete_account))
|
.route("/api/accounts", delete(accounts::delete_account))
|
||||||
.route("/api/accounts/delete", post(accounts::delete_account))
|
.route("/api/accounts/delete", post(accounts::delete_account))
|
||||||
|
// Change password
|
||||||
|
.route("/api/accounts/password", post(accounts::post_password))
|
||||||
|
// Rotate encryption keys
|
||||||
|
.route(
|
||||||
|
"/api/accounts/key-management/rotate-user-account-keys",
|
||||||
|
post(accounts::post_rotatekey),
|
||||||
|
)
|
||||||
// Ciphers CRUD
|
// Ciphers CRUD
|
||||||
.route("/api/ciphers", post(ciphers::create_cipher_simple))
|
.route("/api/ciphers", post(ciphers::create_cipher_simple))
|
||||||
.route("/api/ciphers/create", post(ciphers::create_cipher))
|
.route("/api/ciphers/create", post(ciphers::create_cipher))
|
||||||
|
|
@ -38,11 +45,20 @@ pub fn api_router(env: Env) -> Router {
|
||||||
.route("/api/ciphers/{id}/delete", put(ciphers::soft_delete_cipher))
|
.route("/api/ciphers/{id}/delete", put(ciphers::soft_delete_cipher))
|
||||||
// Cipher hard delete (DELETE/POST permanently removes cipher)
|
// Cipher hard delete (DELETE/POST permanently removes cipher)
|
||||||
.route("/api/ciphers/{id}", delete(ciphers::hard_delete_cipher))
|
.route("/api/ciphers/{id}", delete(ciphers::hard_delete_cipher))
|
||||||
.route("/api/ciphers/{id}/delete", post(ciphers::hard_delete_cipher))
|
.route(
|
||||||
|
"/api/ciphers/{id}/delete",
|
||||||
|
post(ciphers::hard_delete_cipher),
|
||||||
|
)
|
||||||
// Cipher bulk soft delete
|
// Cipher bulk soft delete
|
||||||
.route("/api/ciphers/delete", put(ciphers::soft_delete_ciphers_bulk))
|
.route(
|
||||||
|
"/api/ciphers/delete",
|
||||||
|
put(ciphers::soft_delete_ciphers_bulk),
|
||||||
|
)
|
||||||
// Cipher bulk hard delete
|
// Cipher bulk hard delete
|
||||||
.route("/api/ciphers/delete", post(ciphers::hard_delete_ciphers_bulk))
|
.route(
|
||||||
|
"/api/ciphers/delete",
|
||||||
|
post(ciphers::hard_delete_ciphers_bulk),
|
||||||
|
)
|
||||||
.route("/api/ciphers", delete(ciphers::hard_delete_ciphers_bulk))
|
.route("/api/ciphers", delete(ciphers::hard_delete_ciphers_bulk))
|
||||||
// Cipher restore (clears deleted_at)
|
// Cipher restore (clears deleted_at)
|
||||||
.route("/api/ciphers/{id}/restore", put(ciphers::restore_cipher))
|
.route("/api/ciphers/{id}/restore", put(ciphers::restore_cipher))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue