Merge branch 'BlackDex-future-web-vault' into main
This commit is contained in:
commit
96c2416903
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN private_key TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN public_key TEXT;
|
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN private_key TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN public_key TEXT;
|
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN private_key TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN public_key TEXT;
|
|
@ -231,7 +231,10 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash, Some("post_rotatekey"));
|
user.set_password(
|
||||||
|
&data.NewMasterPasswordHash,
|
||||||
|
Some(vec![String::from("post_rotatekey"), String::from("get_contacts")]),
|
||||||
|
);
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
@ -320,7 +323,9 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
||||||
err!("The cipher is not owned by the user")
|
err!("The cipher is not owned by the user")
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherUpdate)?
|
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||||
|
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
||||||
|
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::None)?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user data
|
// Update user data
|
||||||
|
@ -329,7 +334,6 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
user.private_key = Some(data.PrivateKey);
|
user.private_key = Some(data.PrivateKey);
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
user.reset_stamp_exception();
|
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
use rocket::Route;
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
|
use crate::{api::JsonResult, auth::Headers, db::DbConn};
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![get_contacts,]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This endpoint is expected to return at least something.
|
||||||
|
/// If we return an error message that will trigger error toasts for the user.
|
||||||
|
/// To prevent this we just return an empty json result with no Data.
|
||||||
|
/// When this feature is going to be implemented it also needs to return this empty Data
|
||||||
|
/// instead of throwing an error/4XX unless it really is an error.
|
||||||
|
#[get("/emergency-access/trusted")]
|
||||||
|
fn get_contacts(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||||
|
debug!("Emergency access is not supported.");
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Data": [],
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": null
|
||||||
|
})))
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod accounts;
|
mod accounts;
|
||||||
mod ciphers;
|
mod ciphers;
|
||||||
|
mod emergency_access;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
mod sends;
|
mod sends;
|
||||||
|
@ -15,6 +16,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
routes.append(&mut accounts::routes());
|
routes.append(&mut accounts::routes());
|
||||||
routes.append(&mut ciphers::routes());
|
routes.append(&mut ciphers::routes());
|
||||||
|
routes.append(&mut emergency_access::routes());
|
||||||
routes.append(&mut folders::routes());
|
routes.append(&mut folders::routes());
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
|
|
|
@ -51,6 +51,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
get_plans,
|
get_plans,
|
||||||
get_plans_tax_rates,
|
get_plans_tax_rates,
|
||||||
import,
|
import,
|
||||||
|
post_org_keys,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ struct OrgData {
|
||||||
CollectionName: String,
|
CollectionName: String,
|
||||||
Key: String,
|
Key: String,
|
||||||
Name: String,
|
Name: String,
|
||||||
|
Keys: Option<OrgKeyData>,
|
||||||
#[serde(rename = "PlanType")]
|
#[serde(rename = "PlanType")]
|
||||||
_PlanType: NumberOrString, // Ignored, always use the same plan
|
_PlanType: NumberOrString, // Ignored, always use the same plan
|
||||||
}
|
}
|
||||||
|
@ -78,6 +80,13 @@ struct NewCollectionData {
|
||||||
Name: String,
|
Name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgKeyData {
|
||||||
|
EncryptedPrivateKey: String,
|
||||||
|
PublicKey: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/organizations", data = "<data>")]
|
#[post("/organizations", data = "<data>")]
|
||||||
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
|
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
|
||||||
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
|
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
|
||||||
|
@ -85,8 +94,14 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: OrgData = data.into_inner().data;
|
let data: OrgData = data.into_inner().data;
|
||||||
|
let (private_key, public_key) = if data.Keys.is_some() {
|
||||||
|
let keys: OrgKeyData = data.Keys.unwrap();
|
||||||
|
(Some(keys.EncryptedPrivateKey), Some(keys.PublicKey))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
let org = Organization::new(data.Name, data.BillingEmail);
|
let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key);
|
||||||
let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone());
|
let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone());
|
||||||
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
||||||
|
|
||||||
|
@ -468,6 +483,32 @@ fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) ->
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/<org_id>/keys", data = "<data>")]
|
||||||
|
fn post_org_keys(org_id: String, data: JsonUpcase<OrgKeyData>, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||||
|
let data: OrgKeyData = data.into_inner().data;
|
||||||
|
|
||||||
|
let mut org = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
|
Some(organization) => {
|
||||||
|
if organization.private_key.is_some() && organization.public_key.is_some() {
|
||||||
|
err!("Organization Keys already exist")
|
||||||
|
}
|
||||||
|
organization
|
||||||
|
}
|
||||||
|
None => err!("Can't find organization details"),
|
||||||
|
};
|
||||||
|
|
||||||
|
org.private_key = Some(data.EncryptedPrivateKey);
|
||||||
|
org.public_key = Some(data.PublicKey);
|
||||||
|
|
||||||
|
org.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "organizationKeys",
|
||||||
|
"PublicKey": org.public_key,
|
||||||
|
"PrivateKey": org.private_key,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct CollectionData {
|
struct CollectionData {
|
||||||
|
|
|
@ -166,8 +166,8 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
|
||||||
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
|
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
|
||||||
enforce_disable_hide_email_policy(&data.data, &headers, &conn)?;
|
enforce_disable_hide_email_policy(&data.data, &headers, &conn)?;
|
||||||
|
|
||||||
// Get the file length and add an extra 10% to avoid issues
|
// Get the file length and add an extra 5% to avoid issues
|
||||||
const SIZE_110_MB: u64 = 115_343_360;
|
const SIZE_525_MB: u64 = 550_502_400;
|
||||||
|
|
||||||
let size_limit = match CONFIG.user_attachment_limit() {
|
let size_limit = match CONFIG.user_attachment_limit() {
|
||||||
Some(0) => err!("File uploads are disabled"),
|
Some(0) => err!("File uploads are disabled"),
|
||||||
|
@ -176,9 +176,9 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
|
||||||
if left <= 0 {
|
if left <= 0 {
|
||||||
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
||||||
}
|
}
|
||||||
std::cmp::Ord::max(left as u64, SIZE_110_MB)
|
std::cmp::Ord::max(left as u64, SIZE_525_MB)
|
||||||
}
|
}
|
||||||
None => SIZE_110_MB,
|
None => SIZE_525_MB,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the Send
|
// Create the Send
|
||||||
|
|
15
src/auth.rs
15
src/auth.rs
|
@ -325,8 +325,19 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||||
_ => err_handler!("Error getting current route for stamp exception"),
|
_ => err_handler!("Error getting current route for stamp exception"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if both match, if not this route is not allowed with the current security stamp.
|
// Check if the stamp exception has expired first.
|
||||||
if stamp_exception.route != current_route {
|
// Then, check if the current route matches any of the allowed routes.
|
||||||
|
// After that check the stamp in exception matches the one in the claims.
|
||||||
|
if Utc::now().naive_utc().timestamp() > stamp_exception.expire {
|
||||||
|
// If the stamp exception has been expired remove it from the database.
|
||||||
|
// This prevents checking this stamp exception for new requests.
|
||||||
|
let mut user = user;
|
||||||
|
user.reset_stamp_exception();
|
||||||
|
if let Err(e) = user.save(&conn) {
|
||||||
|
error!("Error updating user: {:#?}", e);
|
||||||
|
}
|
||||||
|
err_handler!("Stamp exception is expired")
|
||||||
|
} else if !stamp_exception.routes.contains(¤t_route.to_string()) {
|
||||||
err_handler!("Invalid security stamp: Current route and exception route do not match")
|
err_handler!("Invalid security stamp: Current route and exception route do not match")
|
||||||
} else if stamp_exception.security_stamp != claims.sstamp {
|
} else if stamp_exception.security_stamp != claims.sstamp {
|
||||||
err_handler!("Invalid security stamp for matched stamp exception")
|
err_handler!("Invalid security stamp for matched stamp exception")
|
||||||
|
|
|
@ -12,6 +12,8 @@ db_object! {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub billing_email: String,
|
pub billing_email: String,
|
||||||
|
pub private_key: Option<String>,
|
||||||
|
pub public_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
|
@ -122,12 +124,13 @@ impl PartialOrd<UserOrgType> for i32 {
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Organization {
|
impl Organization {
|
||||||
pub fn new(name: String, billing_email: String) -> Self {
|
pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid: crate::util::get_uuid(),
|
uuid: crate::util::get_uuid(),
|
||||||
|
|
||||||
name,
|
name,
|
||||||
billing_email,
|
billing_email,
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,14 +143,16 @@ impl Organization {
|
||||||
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
"UseDirectory": false,
|
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||||
"UseEvents": false,
|
"UseEvents": false, // not supported by us
|
||||||
"UseGroups": false,
|
"UseGroups": false, // not supported by us
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
"UseSso": false, // We do not support SSO
|
"UseSso": false, // We do not support SSO
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"UseApi": false, // not supported by us
|
"UseApi": false, // not supported by us
|
||||||
|
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||||
|
"ResetPasswordEnrolled": false, // not supported by us
|
||||||
|
|
||||||
"BusinessName": null,
|
"BusinessName": null,
|
||||||
"BusinessAddress1": null,
|
"BusinessAddress1": null,
|
||||||
|
@ -269,13 +274,15 @@ impl UserOrganization {
|
||||||
"UsersGetPremium": true,
|
"UsersGetPremium": true,
|
||||||
|
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
"UseDirectory": false,
|
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||||
"UseEvents": false,
|
"UseEvents": false, // not supported by us
|
||||||
"UseGroups": false,
|
"UseGroups": false, // not supported by us
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
"UseApi": false, // not supported by us
|
"UseApi": false, // not supported by us
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
|
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||||
|
"ResetPasswordEnrolled": false, // not supported by us
|
||||||
"SsoBound": false, // We do not support SSO
|
"SsoBound": false, // We do not support SSO
|
||||||
"UseSso": false, // We do not support SSO
|
"UseSso": false, // We do not support SSO
|
||||||
// TODO: Add support for Business Portal
|
// TODO: Add support for Business Portal
|
||||||
|
@ -293,10 +300,12 @@ impl UserOrganization {
|
||||||
// "AccessReports": false,
|
// "AccessReports": false,
|
||||||
// "ManageAllCollections": false,
|
// "ManageAllCollections": false,
|
||||||
// "ManageAssignedCollections": false,
|
// "ManageAssignedCollections": false,
|
||||||
|
// "ManageCiphers": false,
|
||||||
// "ManageGroups": false,
|
// "ManageGroups": false,
|
||||||
// "ManagePolicies": false,
|
// "ManagePolicies": false,
|
||||||
|
// "ManageResetPassword": false,
|
||||||
// "ManageSso": false,
|
// "ManageSso": false,
|
||||||
// "ManageUsers": false
|
// "ManageUsers": false,
|
||||||
// },
|
// },
|
||||||
|
|
||||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
|
@ -63,8 +63,9 @@ enum UserStatus {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct UserStampException {
|
pub struct UserStampException {
|
||||||
pub route: String,
|
pub routes: Vec<String>,
|
||||||
pub security_stamp: String,
|
pub security_stamp: String,
|
||||||
|
pub expire: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
@ -135,9 +136,11 @@ impl User {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `password` - A str which contains a hashed version of the users master password.
|
/// * `password` - A str which contains a hashed version of the users master password.
|
||||||
/// * `allow_next_route` - A Option<&str> with the function name of the next allowed (rocket) route.
|
/// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.
|
||||||
|
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||||
|
/// After these 2 minutes this stamp will expire.
|
||||||
///
|
///
|
||||||
pub fn set_password(&mut self, password: &str, allow_next_route: Option<&str>) {
|
pub fn set_password(&mut self, password: &str, allow_next_route: Option<Vec<String>>) {
|
||||||
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
||||||
|
|
||||||
if let Some(route) = allow_next_route {
|
if let Some(route) = allow_next_route {
|
||||||
|
@ -154,24 +157,20 @@ impl User {
|
||||||
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
|
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `route_exception` - A str with the function name of the next allowed (rocket) route.
|
/// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.
|
||||||
|
/// These routes are able to use the previous stamp id for the next 2 minutes.
|
||||||
|
/// After these 2 minutes this stamp will expire.
|
||||||
///
|
///
|
||||||
/// ### Future
|
pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {
|
||||||
/// In the future it could be posible that we need more of these exception routes.
|
|
||||||
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
|
|
||||||
pub fn set_stamp_exception(&mut self, route_exception: &str) {
|
|
||||||
let stamp_exception = UserStampException {
|
let stamp_exception = UserStampException {
|
||||||
route: route_exception.to_string(),
|
routes: route_exception,
|
||||||
security_stamp: self.security_stamp.to_string(),
|
security_stamp: self.security_stamp.to_string(),
|
||||||
|
expire: (Utc::now().naive_utc() + Duration::minutes(2)).timestamp(),
|
||||||
};
|
};
|
||||||
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
|
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the stamp_exception to prevent re-use of the previous security-stamp
|
/// Resets the stamp_exception to prevent re-use of the previous security-stamp
|
||||||
///
|
|
||||||
/// ### Future
|
|
||||||
/// In the future it could be posible that we need more of these exception routes.
|
|
||||||
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
|
|
||||||
pub fn reset_stamp_exception(&mut self) {
|
pub fn reset_stamp_exception(&mut self) {
|
||||||
self.stamp_exception = None;
|
self.stamp_exception = None;
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,8 @@ table! {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
billing_email -> Text,
|
billing_email -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,8 @@ table! {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
billing_email -> Text,
|
billing_email -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,8 @@ table! {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
billing_email -> Text,
|
billing_email -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,6 +174,9 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
|
||||||
"Message": msg,
|
"Message": msg,
|
||||||
"Object": "error"
|
"Object": "error"
|
||||||
},
|
},
|
||||||
|
"ExceptionMessage": null,
|
||||||
|
"ExceptionStackTrace": null,
|
||||||
|
"InnerExceptionMessage": null,
|
||||||
"Object": "error"
|
"Object": "error"
|
||||||
});
|
});
|
||||||
_serialize(&json, "")
|
_serialize(&json, "")
|
||||||
|
|
Loading…
Reference in New Issue