feat: align with vaultwarden 1.35.0
**Synced Vaultwarden Changes**: - `061d320c` - Add new accountKeys and masterPasswordUnlock fields - `5c91058b` - Add UserDecryptionOptions on /sync too - `57bdab15` - add empty /api/tasks endpoint - `0ab7784b` - Update web-vault to v2025.12.0
This commit is contained in:
parent
bba7bbc8a2
commit
26f9da5218
8 changed files with 175 additions and 62 deletions
|
|
@ -6,7 +6,7 @@ use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use worker::{query, D1PreparedStatement, Env};
|
use worker::{query, D1PreparedStatement, Env};
|
||||||
|
|
||||||
use super::{get_batch_size, server_password_iterations};
|
use super::{get_batch_size, server_password_iterations, two_factor_enabled};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::Claims,
|
auth::Claims,
|
||||||
crypto::{generate_salt, hash_password_for_storage},
|
crypto::{generate_salt, hash_password_for_storage},
|
||||||
|
|
@ -331,6 +331,18 @@ pub async fn revision_date(
|
||||||
Ok(Json(revision_date))
|
Ok(Json(revision_date))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /api/accounts/tasks
|
||||||
|
///
|
||||||
|
/// Vaultwarden returns an empty list here; some official clients call this endpoint.
|
||||||
|
/// We don't implement task workflows, so always return an empty list.
|
||||||
|
#[worker::send]
|
||||||
|
pub async fn get_tasks() -> Result<Json<Value>, AppError> {
|
||||||
|
Ok(Json(json!({
|
||||||
|
"data": [],
|
||||||
|
"object": "list"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[worker::send]
|
#[worker::send]
|
||||||
pub async fn get_profile(
|
pub async fn get_profile(
|
||||||
claims: Claims,
|
claims: Claims,
|
||||||
|
|
@ -341,12 +353,13 @@ pub async fn get_profile(
|
||||||
|
|
||||||
let user: User = db
|
let user: User = db
|
||||||
.prepare("SELECT * FROM users WHERE id = ?1")
|
.prepare("SELECT * FROM users WHERE id = ?1")
|
||||||
.bind(&[user_id.into()])?
|
.bind(&[user_id.clone().into()])?
|
||||||
.first(None)
|
.first(None)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
||||||
|
|
||||||
let profile = Profile::from_user(user)?;
|
let two_factor_enabled = two_factor_enabled(&db, &user_id).await?;
|
||||||
|
let profile = Profile::from_user(user, two_factor_enabled)?;
|
||||||
|
|
||||||
Ok(Json(profile))
|
Ok(Json(profile))
|
||||||
}
|
}
|
||||||
|
|
@ -392,7 +405,8 @@ pub async fn post_profile(
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AppError::Database)?;
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
let profile = Profile::from_user(user)?;
|
let two_factor_enabled = two_factor_enabled(&db, user_id).await?;
|
||||||
|
let profile = Profile::from_user(user, two_factor_enabled)?;
|
||||||
|
|
||||||
Ok(Json(profile))
|
Ok(Json(profile))
|
||||||
}
|
}
|
||||||
|
|
@ -450,7 +464,8 @@ pub async fn put_avatar(
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AppError::Database)?;
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
let profile = Profile::from_user(user)?;
|
let two_factor_enabled = two_factor_enabled(&db, user_id).await?;
|
||||||
|
let profile = Profile::from_user(user, two_factor_enabled)?;
|
||||||
|
|
||||||
Ok(Json(profile))
|
Ok(Json(profile))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ pub async fn config(
|
||||||
// We should make sure that we keep this updated when we support the new server features
|
// We should make sure that we keep this updated when we support the new server features
|
||||||
// Version history:
|
// Version history:
|
||||||
// - Individual cipher key encryption: 2024.2.0
|
// - Individual cipher key encryption: 2024.2.0
|
||||||
"version": "2025.6.0",
|
// - Mobile app support for MasterPasswordUnlockData: 2025.8.0
|
||||||
|
"version": "2025.12.0",
|
||||||
"gitHash": "5d84f176",
|
"gitHash": "5d84f176",
|
||||||
"server": {
|
"server": {
|
||||||
"name": "Vaultwarden",
|
"name": "Vaultwarden",
|
||||||
|
|
@ -53,8 +54,8 @@ pub async fn config(
|
||||||
"vault": domain,
|
"vault": domain,
|
||||||
"api": format!("{domain}/api"),
|
"api": format!("{domain}/api"),
|
||||||
"identity": format!("{domain}/identity"),
|
"identity": format!("{domain}/identity"),
|
||||||
"notifications": format!("{domain}/notifications"),
|
"notifications": format!(""),
|
||||||
"sso": format!("{domain}/sso"),
|
"sso": format!(""),
|
||||||
"cloudRegion": null,
|
"cloudRegion": null,
|
||||||
},
|
},
|
||||||
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
|
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ use crate::{
|
||||||
crypto::{ct_eq, generate_salt, hash_password_for_storage, validate_totp},
|
crypto::{ct_eq, generate_salt, hash_password_for_storage, validate_totp},
|
||||||
db,
|
db,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
handlers::{allow_totp_drift, server_password_iterations},
|
handlers::{
|
||||||
|
allow_totp_drift,
|
||||||
|
server_password_iterations,
|
||||||
|
twofactor::{is_twofactor_enabled, list_user_twofactors},
|
||||||
|
},
|
||||||
models::twofactor::{RememberTokenData, TwoFactor, TwoFactorType},
|
models::twofactor::{RememberTokenData, TwoFactor, TwoFactorType},
|
||||||
models::user::User,
|
models::user::User,
|
||||||
};
|
};
|
||||||
|
|
@ -94,6 +98,8 @@ pub struct TokenResponse {
|
||||||
force_password_reset: bool,
|
force_password_reset: bool,
|
||||||
#[serde(rename = "UserDecryptionOptions")]
|
#[serde(rename = "UserDecryptionOptions")]
|
||||||
user_decryption_options: UserDecryptionOptions,
|
user_decryption_options: UserDecryptionOptions,
|
||||||
|
#[serde(rename = "AccountKeys")]
|
||||||
|
account_keys: serde_json::Value,
|
||||||
#[serde(rename = "TwoFactorToken", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "TwoFactorToken", skip_serializing_if = "Option::is_none")]
|
||||||
two_factor_token: Option<String>,
|
two_factor_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +108,8 @@ pub struct TokenResponse {
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct UserDecryptionOptions {
|
pub struct UserDecryptionOptions {
|
||||||
pub has_master_password: bool,
|
pub has_master_password: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub master_password_unlock: Option<serde_json::Value>,
|
||||||
pub object: String,
|
pub object: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,6 +159,34 @@ fn generate_tokens_and_response(
|
||||||
&EncodingKey::from_secret(jwt_refresh_secret.as_ref()),
|
&EncodingKey::from_secret(jwt_refresh_secret.as_ref()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let has_master_password = !user.master_password_hash.is_empty();
|
||||||
|
let master_password_unlock = if has_master_password {
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"Kdf": {
|
||||||
|
"KdfType": user.kdf_type,
|
||||||
|
"Iterations": user.kdf_iterations,
|
||||||
|
"Memory": user.kdf_memory,
|
||||||
|
"Parallelism": user.kdf_parallelism
|
||||||
|
},
|
||||||
|
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
|
||||||
|
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
|
||||||
|
"MasterKeyEncryptedUserKey": user.key,
|
||||||
|
"MasterKeyWrappedUserKey": user.key,
|
||||||
|
"Salt": user.email
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let account_keys = serde_json::json!({
|
||||||
|
"publicKeyEncryptionKeyPair": {
|
||||||
|
"wrappedPrivateKey": user.private_key,
|
||||||
|
"publicKey": user.public_key,
|
||||||
|
"Object": "publicKeyEncryptionKeyPair"
|
||||||
|
},
|
||||||
|
"Object": "privateKeys"
|
||||||
|
});
|
||||||
|
|
||||||
Ok(Json(TokenResponse {
|
Ok(Json(TokenResponse {
|
||||||
access_token,
|
access_token,
|
||||||
expires_in: expires_in.num_seconds(),
|
expires_in: expires_in.num_seconds(),
|
||||||
|
|
@ -165,9 +201,11 @@ fn generate_tokens_and_response(
|
||||||
force_password_reset: false,
|
force_password_reset: false,
|
||||||
reset_master_password: false,
|
reset_master_password: false,
|
||||||
user_decryption_options: UserDecryptionOptions {
|
user_decryption_options: UserDecryptionOptions {
|
||||||
has_master_password: true,
|
has_master_password,
|
||||||
|
master_password_unlock,
|
||||||
object: "userDecryptionOptions".to_string(),
|
object: "userDecryptionOptions".to_string(),
|
||||||
},
|
},
|
||||||
|
account_keys,
|
||||||
two_factor_token,
|
two_factor_token,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -215,51 +253,32 @@ pub async fn token(
|
||||||
return Err(AppError::Unauthorized("Invalid credentials".to_string()));
|
return Err(AppError::Unauthorized("Invalid credentials".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for 2FA
|
// Check for 2FA (TOTP) for this user.
|
||||||
let twofactors: Vec<TwoFactor> = db
|
let twofactors: Vec<TwoFactor> = list_user_twofactors(&db, &user.id).await?;
|
||||||
.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype < 1000")
|
|
||||||
.bind(&[user.id.clone().into()])?
|
|
||||||
.all()
|
|
||||||
.await
|
|
||||||
.map_err(|_| AppError::Database)?
|
|
||||||
.results()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut two_factor_remember_token: Option<String> = None;
|
let mut two_factor_remember_token: Option<String> = None;
|
||||||
|
|
||||||
// Filter out Remember type - it's not a real 2FA provider, just a convenience feature
|
if is_twofactor_enabled(&twofactors) {
|
||||||
let real_twofactors: Vec<&TwoFactor> = twofactors
|
// Only advertise Authenticator (TOTP) as the real provider for now.
|
||||||
.iter()
|
let twofactor_ids: Vec<i32> = vec![TwoFactorType::Authenticator as i32];
|
||||||
.filter(|tf| tf.atype != TwoFactorType::Remember as i32)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !real_twofactors.is_empty() {
|
|
||||||
// 2FA is enabled, need to verify
|
|
||||||
// Only show real 2FA providers to client (not Remember)
|
|
||||||
let twofactor_ids: Vec<i32> = real_twofactors.iter().map(|tf| tf.atype).collect();
|
|
||||||
let selected_id = payload.two_factor_provider.unwrap_or(twofactor_ids[0]);
|
let selected_id = payload.two_factor_provider.unwrap_or(twofactor_ids[0]);
|
||||||
|
|
||||||
let twofactor_code = match &payload.two_factor_token {
|
let twofactor_code = match &payload.two_factor_token {
|
||||||
Some(code) => code,
|
Some(code) => code,
|
||||||
None => {
|
None => {
|
||||||
// Return 2FA required error
|
// Return 2FA required error
|
||||||
return Err(AppError::TwoFactorRequired(json_err_twofactor(
|
return Err(AppError::TwoFactorRequired(json_err_twofactor(&twofactor_ids)));
|
||||||
&twofactor_ids,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the selected twofactor from real_twofactors
|
|
||||||
let selected_twofactor = real_twofactors
|
|
||||||
.iter()
|
|
||||||
.find(|tf| tf.atype == selected_id && tf.enabled)
|
|
||||||
.copied();
|
|
||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => {
|
Some(TwoFactorType::Authenticator) => {
|
||||||
let tf = selected_twofactor.ok_or_else(|| {
|
let tf = twofactors
|
||||||
AppError::BadRequest("TOTP not configured".to_string())
|
.iter()
|
||||||
})?;
|
.find(|tf| {
|
||||||
|
tf.enabled && tf.atype == TwoFactorType::Authenticator as i32
|
||||||
|
})
|
||||||
|
.ok_or_else(|| AppError::BadRequest("TOTP not configured".to_string()))?;
|
||||||
|
|
||||||
// Validate TOTP code
|
// Validate TOTP code
|
||||||
let allow_drift = allow_totp_drift(&env);
|
let allow_drift = allow_totp_drift(&env);
|
||||||
|
|
@ -283,10 +302,9 @@ pub async fn token(
|
||||||
// Remember is handled separately - client sends remember token from previous login
|
// Remember is handled separately - client sends remember token from previous login
|
||||||
// Check remember token against stored value for this device
|
// Check remember token against stored value for this device
|
||||||
if let Some(ref device_id) = payload.device_identifier {
|
if let Some(ref device_id) = payload.device_identifier {
|
||||||
// Find remember token in twofactors (not real_twofactors)
|
let remember_tf = twofactors.iter().find(|tf| {
|
||||||
let remember_tf = twofactors
|
tf.enabled && tf.atype == TwoFactorType::Remember as i32
|
||||||
.iter()
|
});
|
||||||
.find(|tf| tf.atype == TwoFactorType::Remember as i32);
|
|
||||||
|
|
||||||
if let Some(tf) = remember_tf {
|
if let Some(tf) = remember_tf {
|
||||||
// Parse stored remember tokens as JSON
|
// Parse stored remember tokens as JSON
|
||||||
|
|
|
||||||
|
|
@ -70,3 +70,12 @@ pub(crate) fn allow_totp_drift(env: &worker::Env) -> bool {
|
||||||
.map(|value| !matches!(value.as_str(), "1" | "true" | "yes" | "on"))
|
.map(|value| !matches!(value.as_str(), "1" | "true" | "yes" | "on"))
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the user has 2FA enabled.
|
||||||
|
pub(crate) async fn two_factor_enabled(
|
||||||
|
db: &worker::D1Database,
|
||||||
|
user_id: &str,
|
||||||
|
) -> Result<bool, crate::error::AppError> {
|
||||||
|
let twofactors = crate::handlers::twofactor::list_user_twofactors(db, user_id).await?;
|
||||||
|
Ok(crate::handlers::twofactor::is_twofactor_enabled(&twofactors))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
auth::Claims,
|
auth::Claims,
|
||||||
db,
|
db,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
handlers::{attachments, ciphers},
|
handlers::{attachments, ciphers, two_factor_enabled},
|
||||||
models::{
|
models::{
|
||||||
folder::{Folder, FolderResponse},
|
folder::{Folder, FolderResponse},
|
||||||
sync::Profile,
|
sync::Profile,
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use ciphers::RawJson;
|
use ciphers::RawJson;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
#[worker::send]
|
#[worker::send]
|
||||||
pub async fn get_sync_data(
|
pub async fn get_sync_data(
|
||||||
|
|
@ -32,6 +33,29 @@ pub async fn get_sync_data(
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
|
||||||
|
|
||||||
|
let two_factor_enabled = two_factor_enabled(&db, &user_id).await?;
|
||||||
|
|
||||||
|
let has_master_password = !user.master_password_hash.is_empty();
|
||||||
|
let master_password_unlock = if has_master_password {
|
||||||
|
// Mirrors vaultwarden's `ciphers::sync` casing (lower camelCase).
|
||||||
|
// We don't support SSO, so this is always derived from the current user record.
|
||||||
|
json!({
|
||||||
|
"kdf": {
|
||||||
|
"kdfType": user.kdf_type,
|
||||||
|
"iterations": user.kdf_iterations,
|
||||||
|
"memory": user.kdf_memory,
|
||||||
|
"parallelism": user.kdf_parallelism
|
||||||
|
},
|
||||||
|
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
|
||||||
|
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
|
||||||
|
"masterKeyEncryptedUserKey": user.key,
|
||||||
|
"masterKeyWrappedUserKey": user.key,
|
||||||
|
"salt": user.email
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Value::Null
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch folders
|
// Fetch folders
|
||||||
let folders_db: Vec<Folder> = db
|
let folders_db: Vec<Folder> = db
|
||||||
.prepare("SELECT * FROM folders WHERE user_id = ?1")
|
.prepare("SELECT * FROM folders WHERE user_id = ?1")
|
||||||
|
|
@ -54,14 +78,22 @@ pub async fn get_sync_data(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Serialize profile and folders (small data, acceptable CPU cost)
|
// Serialize profile and folders (small data, acceptable CPU cost)
|
||||||
let profile = Profile::from_user(user)?;
|
let mut profile = Profile::from_user(user, two_factor_enabled)?;
|
||||||
|
// Match vaultwarden semantics: `_status` is `Invited` when no master password is set.
|
||||||
|
// We don't implement org invitations, but this helps clients interpret the account state.
|
||||||
|
profile.status = if has_master_password { 0 } else { 1 };
|
||||||
let profile_json = serde_json::to_string(&profile).map_err(|_| AppError::Internal)?;
|
let profile_json = serde_json::to_string(&profile).map_err(|_| AppError::Internal)?;
|
||||||
let folders_json = serde_json::to_string(&folders).map_err(|_| AppError::Internal)?;
|
let folders_json = serde_json::to_string(&folders).map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
// Build response JSON via string concatenation (ciphers already raw JSON)
|
// Build response JSON via string concatenation (ciphers already raw JSON)
|
||||||
|
let user_decryption_json = serde_json::to_string(&json!({
|
||||||
|
"masterPasswordUnlock": master_password_unlock
|
||||||
|
}))
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
let response = format!(
|
let response = format!(
|
||||||
r#"{{"profile":{},"folders":{},"collections":[],"policies":[],"ciphers":{},"domains":null,"sends":[],"object":"sync"}}"#,
|
r#"{{"profile":{},"folders":{},"collections":[],"policies":[],"ciphers":{},"domains":null,"sends":[],"userDecryption":{},"object":"sync"}}"#,
|
||||||
profile_json, folders_json, ciphers_json
|
profile_json, folders_json, ciphers_json, user_decryption_json
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(RawJson(response))
|
Ok(RawJson(response))
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,30 @@ use crate::{
|
||||||
models::user::{PasswordOrOtpData, User},
|
models::user::{PasswordOrOtpData, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// List all 2FA records for a user (includes Remember tokens, excludes atype >= 1000).
|
||||||
|
pub(crate) async fn list_user_twofactors(
|
||||||
|
db: &worker::D1Database,
|
||||||
|
user_id: &str,
|
||||||
|
) -> Result<Vec<TwoFactor>, AppError> {
|
||||||
|
db.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype < 1000")
|
||||||
|
.bind(&[user_id.to_string().into()])?
|
||||||
|
.all()
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.results::<TwoFactor>()
|
||||||
|
.map_err(|_| AppError::Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the user has 2FA enabled.
|
||||||
|
///
|
||||||
|
/// For now, we intentionally only treat Authenticator (TOTP) as a real 2FA provider.
|
||||||
|
/// Remember-device tokens are never considered a 2FA method by themselves.
|
||||||
|
pub(crate) fn is_twofactor_enabled(twofactors: &[TwoFactor]) -> bool {
|
||||||
|
twofactors.iter().any(|tf| {
|
||||||
|
tf.enabled && tf.atype == TwoFactorType::Authenticator as i32
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// GET /api/two-factor - Get all enabled 2FA providers for current user
|
/// GET /api/two-factor - Get all enabled 2FA providers for current user
|
||||||
#[worker::send]
|
#[worker::send]
|
||||||
pub async fn get_twofactor(
|
pub async fn get_twofactor(
|
||||||
|
|
@ -24,17 +48,8 @@ pub async fn get_twofactor(
|
||||||
) -> Result<Json<Value>, AppError> {
|
) -> Result<Json<Value>, AppError> {
|
||||||
let db = db::get_db(&env)?;
|
let db = db::get_db(&env)?;
|
||||||
|
|
||||||
let twofactors: Vec<Value> = db
|
let twofactors = list_user_twofactors(&db, &user_id).await?;
|
||||||
.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype < 1000")
|
let twofactors: Vec<Value> = twofactors.iter().map(|tf| tf.to_json_provider()).collect();
|
||||||
.bind(&[user_id.clone().into()])?
|
|
||||||
.all()
|
|
||||||
.await
|
|
||||||
.map_err(|_| AppError::Database)?
|
|
||||||
.results::<TwoFactor>()
|
|
||||||
.map_err(|_| AppError::Database)?
|
|
||||||
.iter()
|
|
||||||
.map(|tf| tf.to_json_provider())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"data": twofactors,
|
"data": twofactors,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ pub struct Profile {
|
||||||
pub avatar_color: Option<String>,
|
pub avatar_color: Option<String>,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
pub kdf: i32,
|
||||||
|
pub kdf_iterations: i32,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub kdf_memory: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub kdf_parallelism: Option<i32>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub master_password_hint: Option<String>,
|
pub master_password_hint: Option<String>,
|
||||||
pub security_stamp: String,
|
pub security_stamp: String,
|
||||||
|
|
@ -26,10 +32,18 @@ pub struct Profile {
|
||||||
pub creation_date: String,
|
pub creation_date: String,
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
pub key: String,
|
pub key: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub organizations: Vec<Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub providers: Vec<Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub provider_organizations: Vec<Value>,
|
||||||
|
#[serde(rename = "_status")]
|
||||||
|
pub status: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
pub fn from_user(user: User) -> Result<Self, AppError> {
|
pub fn from_user(user: User, two_factor_enabled: bool) -> Result<Self, AppError> {
|
||||||
let creation_date = chrono::DateTime::parse_from_rfc3339(&user.created_at)
|
let creation_date = chrono::DateTime::parse_from_rfc3339(&user.created_at)
|
||||||
.map_err(|_| AppError::Internal)?
|
.map_err(|_| AppError::Internal)?
|
||||||
.to_rfc3339_opts(SecondsFormat::Micros, true);
|
.to_rfc3339_opts(SecondsFormat::Micros, true);
|
||||||
|
|
@ -39,18 +53,26 @@ impl Profile {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatar_color: user.avatar_color,
|
avatar_color: user.avatar_color,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
kdf: user.kdf_type,
|
||||||
|
kdf_iterations: user.kdf_iterations,
|
||||||
|
kdf_memory: user.kdf_memory,
|
||||||
|
kdf_parallelism: user.kdf_parallelism,
|
||||||
master_password_hint: user.master_password_hint,
|
master_password_hint: user.master_password_hint,
|
||||||
security_stamp: user.security_stamp,
|
security_stamp: user.security_stamp,
|
||||||
object: "profile".to_string(),
|
object: "profile".to_string(),
|
||||||
premium_from_organization: false,
|
premium_from_organization: false,
|
||||||
force_password_reset: false,
|
force_password_reset: false,
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
two_factor_enabled: false,
|
two_factor_enabled,
|
||||||
premium: true,
|
premium: true,
|
||||||
uses_key_connector: false,
|
uses_key_connector: false,
|
||||||
creation_date,
|
creation_date,
|
||||||
private_key: user.private_key,
|
private_key: user.private_key,
|
||||||
key: user.key,
|
key: user.key,
|
||||||
|
organizations: Vec::new(),
|
||||||
|
providers: Vec::new(),
|
||||||
|
provider_organizations: Vec::new(),
|
||||||
|
status: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ pub fn api_router(env: Env) -> Router {
|
||||||
.route("/api/sync", get(sync::get_sync_data))
|
.route("/api/sync", get(sync::get_sync_data))
|
||||||
// For on-demand sync checks
|
// For on-demand sync checks
|
||||||
.route("/api/accounts/revision-date", get(accounts::revision_date))
|
.route("/api/accounts/revision-date", get(accounts::revision_date))
|
||||||
|
.route("/api/accounts/tasks", get(accounts::get_tasks))
|
||||||
.route("/api/accounts/profile", get(accounts::get_profile))
|
.route("/api/accounts/profile", get(accounts::get_profile))
|
||||||
.route("/api/accounts/profile", post(accounts::post_profile))
|
.route("/api/accounts/profile", post(accounts::post_profile))
|
||||||
.route("/api/accounts/profile", put(accounts::put_profile))
|
.route("/api/accounts/profile", put(accounts::put_profile))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue