1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2024-07-02 12:14:41 +02:00

Initial organizations functionality: Creating orgs and inviting users

Dieser Commit ist enthalten in:
Daniel García 2018-04-24 22:01:55 +02:00
Ursprung a4d2aad331
Commit 4093bf92fe
14 geänderte Dateien mit 440 neuen und 170 gelöschten Zeilen

Datei anzeigen

@ -18,12 +18,14 @@ CREATE TABLE users_collections (
); );
CREATE TABLE users_organizations ( CREATE TABLE users_organizations (
uuid TEXT NOT NULL PRIMARY KEY,
user_uuid TEXT NOT NULL REFERENCES users (uuid), user_uuid TEXT NOT NULL REFERENCES users (uuid),
org_uuid TEXT NOT NULL REFERENCES organizations (uuid), org_uuid TEXT NOT NULL REFERENCES organizations (uuid),
access_all BOOLEAN NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
status INTEGER NOT NULL, status INTEGER NOT NULL,
type INTEGER NOT NULL, type INTEGER NOT NULL,
PRIMARY KEY (user_uuid, org_uuid) UNIQUE (user_uuid, org_uuid)
); );

Datei anzeigen

@ -60,8 +60,18 @@ fn register(data: Json<RegisterData>, conn: DbConn) -> EmptyResult {
} }
#[get("/accounts/profile")] #[get("/accounts/profile")]
fn profile(headers: Headers) -> JsonResult { fn profile(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(headers.user.to_json())) Ok(Json(headers.user.to_json(&conn)))
}
#[get("/users/<uuid>/public-key")]
fn get_public_keys(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&uuid, &conn) {
Some(user) => user,
None => err!("User doesn't exist")
};
Ok(Json(json!(user.public_key)))
} }
#[post("/accounts/keys", data = "<data>")] #[post("/accounts/keys", data = "<data>")]
@ -75,7 +85,7 @@ fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult
user.save(&conn); user.save(&conn);
Ok(Json(user.to_json())) Ok(Json(user.to_json(&conn)))
} }
#[derive(Deserialize)] #[derive(Deserialize)]

Datei anzeigen

@ -23,7 +23,7 @@ use CONFIG;
#[get("/sync")] #[get("/sync")]
fn sync(headers: Headers, conn: DbConn) -> JsonResult { fn sync(headers: Headers, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(); let user_json = headers.user.to_json(&conn);
let folders = Folder::find_by_user(&headers.user.uuid, &conn); let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect(); let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
@ -188,7 +188,6 @@ fn copy_values(from: &Value, to: &mut Value) {
for (key, val) in map { for (key, val) in map {
copy_values(val, &mut to[util::upcase_first(key)]); copy_values(val, &mut to[util::upcase_first(key)]);
} }
} else if let Some(array) = from.as_array() { } else if let Some(array) = from.as_array() {
// Initialize array with null values // Initialize array with null values
*to = json!(vec![Value::Null; array.len()]); *to = json!(vec![Value::Null; array.len()]);
@ -196,7 +195,6 @@ fn copy_values(from: &Value, to: &mut Value) {
for (index, val) in array.iter().enumerate() { for (index, val) in array.iter().enumerate() {
copy_values(val, &mut to[index]); copy_values(val, &mut to[index]);
} }
} else { } else {
*to = from.clone(); *to = from.clone();
} }
@ -405,10 +403,10 @@ fn move_cipher_selected(data: Json<Value>, headers: Headers, conn: DbConn) -> Em
} }
None => err!("Folder doesn't exist") None => err!("Folder doesn't exist")
} }
}, }
None => err!("Folder id provided in wrong format") None => err!("Folder id provided in wrong format")
} }
}, }
None => None None => None
}; };

Datei anzeigen

@ -14,6 +14,7 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
register, register,
profile, profile,
get_public_keys,
post_keys, post_keys,
post_password, post_password,
post_sstamp, post_sstamp,
@ -53,7 +54,15 @@ pub fn routes() -> Vec<Route> {
activate_authenticator, activate_authenticator,
disable_authenticator, disable_authenticator,
create_organization,
get_user_collections, get_user_collections,
get_org_collections,
get_org_details,
get_org_users,
get_collection_users,
send_invite,
confirm_invite,
delete_user,
clear_device_token, clear_device_token,
put_device_token, put_device_token,

Datei anzeigen

@ -8,22 +8,34 @@ use db::models::*;
use api::{JsonResult, EmptyResult}; use api::{JsonResult, EmptyResult};
use auth::Headers; use auth::Headers;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgData {
billingEmail: String,
collectionName: String,
key: String,
name: String,
planType: String,
}
#[post("/organizations", data = "<data>")] #[post("/organizations", data = "<data>")]
fn create_organization(headers: Headers, data: Json<Value>, conn: DbConn) -> JsonResult { fn create_organization(headers: Headers, data: Json<OrgData>, conn: DbConn) -> JsonResult {
/* let data: OrgData = data.into_inner();
Data is a JSON Object with the following entries
billingEmail <email>
collectionName <encrypted_collection_name>
key <key>
name <unencrypted_name>
planType free
*/
// We need to add the following key to the users jwt claims let mut org = Organization::new(data.name, data.billingEmail);
// orgowner: "<org-id>" let mut user_org = UserOrganization::new(
headers.user.uuid, org.uuid.clone());
// This function returns organization.to_json(); user_org.key = data.key;
err!("Not implemented") user_org.access_all = true;
user_org.type_ = UserOrgType::Owner as i32;
user_org.status = UserOrgStatus::Confirmed as i32;
org.save(&conn);
user_org.save(&conn);
Ok(Json(org.to_json()))
} }
@ -50,6 +62,35 @@ fn get_org_collections(org_id: String, headers: Headers, conn: DbConn) -> JsonRe
}))) })))
} }
#[derive(FromForm)]
#[allow(non_snake_case)]
struct OrgIdData {
organizationId: String
}
#[get("/ciphers/organization-details?<data>")]
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult {
// Get list of ciphers in org?
Ok(Json(json!({
"Data": [],
"Object": "list"
})))
}
#[get("/organizations/<org_id>/users")]
fn get_org_users(org_id: String, headers: Headers, conn: DbConn) -> JsonResult {
// TODO Check if user in org
let users = UserOrganization::find_by_org(&org_id, &conn);
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_details(&conn)).collect();
Ok(Json(json!({
"Data": users_json,
"Object": "list"
})))
}
#[get("/organizations/<org_id>/collections/<coll_id>/users")] #[get("/organizations/<org_id>/collections/<coll_id>/users")]
fn get_collection_users(org_id: String, coll_id: String, headers: Headers, conn: DbConn) -> JsonResult { fn get_collection_users(org_id: String, coll_id: String, headers: Headers, conn: DbConn) -> JsonResult {
@ -78,10 +119,94 @@ fn get_collection_users(org_id: String, coll_id: String, headers: Headers, conn:
}))) })))
} }
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct InviteCollectionData {
id: String,
readOnly: bool,
}
//******************************************************************************************** #[derive(Deserialize)]
/* #[allow(non_snake_case)]
We need to modify 'GET /api/profile' to return the users organizations, instead of [] struct InviteData {
emails: Vec<String>,
#[serde(rename = "type")]
type_: String,
collections: Vec<InviteCollectionData>,
accessAll: bool,
The elements from that array come from organization.to_json_profile() }
*/
#[post("/organizations/<org_id>/users/invite", data = "<data>")]
fn send_invite(org_id: String, data: Json<InviteData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: InviteData = data.into_inner();
// TODO Check that user is in org and admin or more
for user_opt in data.emails.iter().map(|email| User::find_by_mail(email, &conn)) {
match user_opt {
None => err!("User email does not exist"),
Some(user) => {
// TODO Check that user is not already in org
let mut user_org = UserOrganization::new(
user.uuid, org_id.clone());
if data.accessAll {
user_org.access_all = data.accessAll;
} else {
err!("Select collections unimplemented")
// TODO create Users_collections
}
user_org.type_ = match data.type_.as_ref() {
"Owner" => UserOrgType::Owner,
"Admin" => UserOrgType::Admin,
"User" => UserOrgType::User,
_ => err!("Invalid type")
} as i32;
user_org.save(&conn);
}
}
}
Ok(())
}
#[post("/organizations/<org_id>/users/<user_id>/confirm", data = "<data>")]
fn confirm_invite(org_id: String, user_id: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
// TODO Check that user is in org and admin or more
let mut user_org = match UserOrganization::find_by_user_and_org(
&user_id, &org_id, &conn) {
Some(user_org) => user_org,
None => err!("Can't find user")
};
if user_org.status != UserOrgStatus::Accepted as i32 {
err!("User in invalid state")
}
user_org.status = UserOrgStatus::Confirmed as i32;
user_org.key = match data["key"].as_str() {
Some(key) => key.to_string(),
None => err!("Invalid key provided")
};
user_org.save(&conn);
Ok(())
}
#[post("/organizations/<org_id>/users/<user_id>/delete")]
fn delete_user(org_id: String, user_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
// TODO Check that user is in org and admin or more
// TODO To delete a user you need either:
// - To be yourself
// - To be of a superior type (ex. Owner can delete Admin and User, Admin can delete User)
// Delete users_organizations and users_collections from this org
unimplemented!();
}

Datei anzeigen

@ -19,7 +19,6 @@ pub fn routes() -> Vec<Route> {
#[post("/connect/token", data = "<connect_data>")] #[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult { fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult {
let data = connect_data.get(); let data = connect_data.get();
println!("{:#?}", data);
let mut device = match data.grant_type { let mut device = match data.grant_type {
GrantType::RefreshToken => { GrantType::RefreshToken => {
@ -98,7 +97,9 @@ fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn)
}; };
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let (access_token, expires_in) = device.refresh_tokens(&user); let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
device.save(&conn); device.save(&conn);
Ok(Json(json!({ Ok(Json(json!({

Datei anzeigen

@ -72,6 +72,10 @@ pub struct JWTClaims {
pub email: String, pub email: String,
pub email_verified: bool, pub email_verified: bool,
pub orgowner: Vec<String>,
pub orgadmin: Vec<String>,
pub orguser: Vec<String>,
// user security_stamp // user security_stamp
pub sstamp: String, pub sstamp: String,
// device uuid // device uuid

Datei anzeigen

@ -1,4 +1,3 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use uuid::Uuid; use uuid::Uuid;
@ -18,8 +17,6 @@ pub struct Collection {
/// Local methods /// Local methods
impl Collection { impl Collection {
pub fn new(org_uuid: String, name: String) -> Self { pub fn new(org_uuid: String, name: String) -> Self {
let now = Utc::now().naive_utc();
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: Uuid::new_v4().to_string(),
@ -46,8 +43,6 @@ use db::schema::collections;
/// Database methods /// Database methods
impl Collection { impl Collection {
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(collections::table) match diesel::replace_into(collections::table)
.values(&*self) .values(&*self)
.execute(&**conn) { .execute(&**conn) {

Datei anzeigen

@ -40,7 +40,7 @@ impl Device {
} }
} }
pub fn refresh_tokens(&mut self, user: &super::User) -> (String, i64) { pub fn refresh_tokens(&mut self, user: &super::User, orgs: Vec<super::UserOrganization>) -> (String, i64) {
// If there is no refresh token, we create one // If there is no refresh token, we create one
if self.refresh_token.is_empty() { if self.refresh_token.is_empty() {
use data_encoding::BASE64URL; use data_encoding::BASE64URL;
@ -51,9 +51,14 @@ impl Device {
// Update the expiration of the device and the last update date // Update the expiration of the device and the last update date
let time_now = Utc::now().naive_utc(); let time_now = Utc::now().naive_utc();
self.updated_at = time_now; self.updated_at = time_now;
let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect();
let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect();
let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client // Create the JWT claims struct, to send to the client
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER}; use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
let claims = JWTClaims { let claims = JWTClaims {
@ -61,16 +66,23 @@ impl Device {
exp: (time_now + *DEFAULT_VALIDITY).timestamp(), exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
iss: JWT_ISSUER.to_string(), iss: JWT_ISSUER.to_string(),
sub: user.uuid.to_string(), sub: user.uuid.to_string(),
premium: true, premium: true,
name: user.name.to_string(), name: user.name.to_string(),
email: user.email.to_string(), email: user.email.to_string(),
email_verified: true, email_verified: true,
orgowner,
orgadmin,
orguser,
sstamp: user.security_stamp.to_string(), sstamp: user.security_stamp.to_string(),
device: self.uuid.to_string(), device: self.uuid.to_string(),
scope: vec!["api".into(), "offline_access".into()], scope: vec!["api".into(), "offline_access".into()],
amr: vec!["Application".into()], amr: vec!["Application".into()],
}; };
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds()) (encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
} }
} }

Datei anzeigen

@ -4,8 +4,16 @@ mod device;
mod folder; mod folder;
mod user; mod user;
mod collection;
mod organization;
pub use self::attachment::Attachment; pub use self::attachment::Attachment;
pub use self::cipher::Cipher; pub use self::cipher::Cipher;
pub use self::device::Device; pub use self::device::Device;
pub use self::folder::Folder; pub use self::folder::Folder;
pub use self::user::User; pub use self::user::User;
pub use self::collection::Collection;
pub use self::organization::Organization;
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};

Datei anzeigen

@ -1,116 +0,0 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use uuid::Uuid;
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "organizations"]
#[primary_key(uuid)]
pub struct Organization {
pub uuid: String,
pub name: String,
pub billing_email: String,
pub key: String,
}
/// Local methods
impl Organization {
pub fn new(name: String, billing_email: String, key: String) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid: Uuid::new_v4().to_string(),
name,
billing_email,
key,
}
}
pub fn to_json(&self) -> JsonValue {
json!({
"Id": self.uuid,
"Name": self.name,
"BusinessName": null,
"BusinessAddress1": null,
"BusinessAddress2": null,
"BusinessAddress3": null,
"BusinessCountry": null,
"BusinessTaxNumber": null,
"BillingEmail":self.billing_email,
"Plan": "Free",
"PlanType": 0, // Free plan
"Seats": 10,
"MaxCollections": 10,
"UseGroups": false,
"UseDirectory": false,
"UseEvents": false,
"UseTotp": false,
"Object": "organization",
})
}
pub fn to_json_profile(&self) -> JsonValue {
json!({
"Id": self.uuid,
"Name": self.name,
"Seats": 10,
"MaxCollections": 10,
"UseGroups": false,
"UseDirectory": false,
"UseEvents": false,
"UseTotp": false,
"MaxStorageGb": null,
// These are probably per user
"Key": self.key,
"Status": 2, // Confirmed
"Type": 0, // Owner
"Enabled": true,
"Object": "profileOrganization",
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::organizations;
/// Database methods
impl Organization {
pub fn save(&mut self, conn: &DbConn) -> bool {
self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(organizations::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(organizations::table.filter(
organizations::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
organizations::table
.filter(organizations::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
}
}

214
src/db/models/organization.rs Normale Datei
Datei anzeigen

@ -0,0 +1,214 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "organizations"]
#[primary_key(uuid)]
pub struct Organization {
pub uuid: String,
pub name: String,
pub billing_email: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "users_organizations"]
#[primary_key(uuid)]
pub struct UserOrganization {
pub uuid: String,
pub user_uuid: String,
pub org_uuid: String,
pub access_all: bool,
pub key: String,
pub status: i32,
pub type_: i32,
}
pub enum UserOrgStatus {
Invited = 0,
Accepted = 1,
Confirmed = 2,
}
pub enum UserOrgType {
Owner = 0,
Admin = 1,
User = 2,
}
/// Local methods
impl Organization {
pub fn new(name: String, billing_email: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
name,
billing_email,
}
}
pub fn to_json(&self) -> JsonValue {
json!({
"Id": self.uuid,
"Name": self.name,
"Seats": 10,
"MaxCollections": 10,
"Use2fa": false,
"UseDirectory": false,
"UseEvents": false,
"UseGroups": false,
"UseTotp": false,
"BusinessName": null,
"BusinessAddress1": null,
"BusinessAddress2": null,
"BusinessAddress3": null,
"BusinessCountry": null,
"BusinessTaxNumber": null,
"BillingEmail": self.billing_email,
"Plan": "Free",
"PlanType": 0, // Free plan
"Object": "organization",
})
}
}
impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
user_uuid,
org_uuid,
access_all: false,
key: String::new(),
status: UserOrgStatus::Accepted as i32,
type_: UserOrgType::User as i32,
}
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::organizations;
use db::schema::users_organizations;
/// Database methods
impl Organization {
pub fn save(&mut self, conn: &DbConn) -> bool {
match diesel::replace_into(organizations::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(organizations::table.filter(
organizations::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
organizations::table
.filter(organizations::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
}
}
impl UserOrganization {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
json!({
"Id": self.org_uuid,
"Name": org.name,
"Seats": 10,
"MaxCollections": 10,
"Use2fa": false,
"UseDirectory": false,
"UseEvents": false,
"UseGroups": false,
"UseTotp": false,
"MaxStorageGb": null,
// These are per user
"Key": self.key,
"Status": self.status,
"Type": self.type_,
"Enabled": true,
"Object": "profileOrganization",
})
}
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue {
use super::User;
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({
"Id": self.uuid,
"UserId": user.uuid,
"Name": user.name,
"Email": user.email,
"Status": self.status,
"Type": self.type_,
"AccessAll": true,
"Object": "organizationUserUserDetails",
})
}
pub fn save(&mut self, conn: &DbConn) -> bool {
match diesel::replace_into(users_organizations::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(users_organizations::table.filter(
users_organizations::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading user organizations")
}
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn).expect("Error loading user organizations")
}
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<Self>(&**conn).ok()
}
}

Datei anzeigen

@ -115,8 +115,21 @@ impl User {
true true
} }
} }
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::users;
/// Database methods
impl User {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
use super::UserOrganization;
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
pub fn to_json(&self) -> JsonValue {
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"Name": self.name, "Name": self.name,
@ -129,19 +142,12 @@ impl User {
"Key": self.key, "Key": self.key,
"PrivateKey": self.private_key, "PrivateKey": self.private_key,
"SecurityStamp": self.security_stamp, "SecurityStamp": self.security_stamp,
"Organizations": [], "Organizations": orgs_json,
"Object": "profile" "Object": "profile"
}) })
} }
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::users;
/// Database methods
impl User {
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();

Datei anzeigen

@ -95,9 +95,11 @@ table! {
} }
table! { table! {
users_organizations (user_uuid, org_uuid) { users_organizations (uuid) {
uuid -> Text,
user_uuid -> Text, user_uuid -> Text,
org_uuid -> Text, org_uuid -> Text,
access_all -> Bool,
key -> Text, key -> Text,
status -> Integer, status -> Integer,
#[sql_name = "type"] #[sql_name = "type"]