diff --git a/migrations/2018-02-17-205753_create_collections_and_orgs/up.sql b/migrations/2018-02-17-205753_create_collections_and_orgs/up.sql index a7b32320..29601a4a 100644 --- a/migrations/2018-02-17-205753_create_collections_and_orgs/up.sql +++ b/migrations/2018-02-17-205753_create_collections_and_orgs/up.sql @@ -18,12 +18,14 @@ CREATE TABLE users_collections ( ); CREATE TABLE users_organizations ( - user_uuid TEXT NOT NULL REFERENCES users (uuid), - org_uuid TEXT NOT NULL REFERENCES organizations (uuid), + uuid TEXT NOT NULL PRIMARY KEY, + user_uuid TEXT NOT NULL REFERENCES users (uuid), + org_uuid TEXT NOT NULL REFERENCES organizations (uuid), - key TEXT NOT NULL, - status INTEGER NOT NULL, - type INTEGER NOT NULL, + access_all BOOLEAN NOT NULL, + key TEXT NOT NULL, + status INTEGER NOT NULL, + type INTEGER NOT NULL, - PRIMARY KEY (user_uuid, org_uuid) + UNIQUE (user_uuid, org_uuid) ); diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 275d55a1..5d90ba93 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -60,8 +60,18 @@ fn register(data: Json, conn: DbConn) -> EmptyResult { } #[get("/accounts/profile")] -fn profile(headers: Headers) -> JsonResult { - Ok(Json(headers.user.to_json())) +fn profile(headers: Headers, conn: DbConn) -> JsonResult { + Ok(Json(headers.user.to_json(&conn))) +} + +#[get("/users//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 = "")] @@ -75,7 +85,7 @@ fn post_keys(data: Json, headers: Headers, conn: DbConn) -> JsonResult user.save(&conn); - Ok(Json(user.to_json())) + Ok(Json(user.to_json(&conn))) } #[derive(Deserialize)] diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 3dae6a86..34751208 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -23,7 +23,7 @@ use CONFIG; #[get("/sync")] 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_json: Vec = folders.iter().map(|c| c.to_json()).collect(); @@ -112,7 +112,7 @@ fn post_ciphers(data: Json, headers: Headers, conn: DbConn) -> JsonR Ok(Json(cipher.to_json(&headers.host, &conn))) } -fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Headers, conn: &DbConn) -> EmptyResult { +fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Headers, conn: &DbConn) -> EmptyResult { if let Some(ref folder_id) = data.folderId { match Folder::find_by_uuid(folder_id, conn) { Some(folder) => { @@ -188,7 +188,6 @@ fn copy_values(from: &Value, to: &mut Value) { for (key, val) in map { copy_values(val, &mut to[util::upcase_first(key)]); } - } else if let Some(array) = from.as_array() { // Initialize array with null values *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() { copy_values(val, &mut to[index]); } - } else { *to = from.clone(); } @@ -375,7 +373,7 @@ fn delete_cipher_selected(data: Json, headers: Headers, conn: DbConn) -> let uuids = match data.get("ids") { Some(ids) => match ids.as_array() { - Some(ids) => ids.iter().filter_map(|uuid| {uuid.as_str()}), + Some(ids) => ids.iter().filter_map(|uuid| { uuid.as_str() }), None => err!("Posted ids field is not an array") }, None => err!("Request missing ids field") @@ -405,16 +403,16 @@ fn move_cipher_selected(data: Json, headers: Headers, conn: DbConn) -> Em } None => err!("Folder doesn't exist") } - }, + } None => err!("Folder id provided in wrong format") } - }, + } None => None }; let uuids = match data.get("ids") { Some(ids) => match ids.as_array() { - Some(ids) => ids.iter().filter_map(|uuid| {uuid.as_str()}), + Some(ids) => ids.iter().filter_map(|uuid| { uuid.as_str() }), None => err!("Posted ids field is not an array") }, None => err!("Request missing ids field") diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index cbfb97ad..4bf4674c 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -14,6 +14,7 @@ pub fn routes() -> Vec { routes![ register, profile, + get_public_keys, post_keys, post_password, post_sstamp, @@ -53,7 +54,15 @@ pub fn routes() -> Vec { activate_authenticator, disable_authenticator, + create_organization, get_user_collections, + get_org_collections, + get_org_details, + get_org_users, + get_collection_users, + send_invite, + confirm_invite, + delete_user, clear_device_token, put_device_token, diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6bd5d075..2a00fcb0 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -8,22 +8,34 @@ use db::models::*; use api::{JsonResult, EmptyResult}; use auth::Headers; + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct OrgData { + billingEmail: String, + collectionName: String, + key: String, + name: String, + planType: String, +} + #[post("/organizations", data = "")] -fn create_organization(headers: Headers, data: Json, conn: DbConn) -> JsonResult { - /* - Data is a JSON Object with the following entries - billingEmail - collectionName - key - name - planType free - */ +fn create_organization(headers: Headers, data: Json, conn: DbConn) -> JsonResult { + let data: OrgData = data.into_inner(); - // We need to add the following key to the users jwt claims - // orgowner: "" + let mut org = Organization::new(data.name, data.billingEmail); + let mut user_org = UserOrganization::new( + headers.user.uuid, org.uuid.clone()); - // This function returns organization.to_json(); - err!("Not implemented") + user_org.key = data.key; + 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?")] +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//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 = users.iter().map(|c| c.to_json_details(&conn)).collect(); + + Ok(Json(json!({ + "Data": users_json, + "Object": "list" + }))) +} #[get("/organizations//collections//users")] 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, +} -//******************************************************************************************** -/* - We need to modify 'GET /api/profile' to return the users organizations, instead of [] +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct InviteData { + emails: Vec, + #[serde(rename = "type")] + type_: String, + collections: Vec, + accessAll: bool, - The elements from that array come from organization.to_json_profile() -*/ +} + +#[post("/organizations//users/invite", data = "")] +fn send_invite(org_id: String, data: Json, 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//users//confirm", data = "")] +fn confirm_invite(org_id: String, user_id: String, data: Json, 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//users//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!(); +} \ No newline at end of file diff --git a/src/api/identity.rs b/src/api/identity.rs index e6a7d9be..805c334f 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -19,7 +19,6 @@ pub fn routes() -> Vec { #[post("/connect/token", data = "")] fn login(connect_data: Form, device_type: DeviceType, conn: DbConn) -> JsonResult { let data = connect_data.get(); - println!("{:#?}", data); let mut device = match data.grant_type { GrantType::RefreshToken => { @@ -98,7 +97,9 @@ fn login(connect_data: Form, device_type: DeviceType, conn: DbConn) }; 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); Ok(Json(json!({ diff --git a/src/auth.rs b/src/auth.rs index f411b4c5..ef96b631 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -72,6 +72,10 @@ pub struct JWTClaims { pub email: String, pub email_verified: bool, + pub orgowner: Vec, + pub orgadmin: Vec, + pub orguser: Vec, + // user security_stamp pub sstamp: String, // device uuid diff --git a/src/db/models/org/collection.rs b/src/db/models/collection.rs similarity index 92% rename from src/db/models/org/collection.rs rename to src/db/models/collection.rs index 5c591532..3189948b 100644 --- a/src/db/models/org/collection.rs +++ b/src/db/models/collection.rs @@ -1,4 +1,3 @@ -use chrono::{NaiveDateTime, Utc}; use serde_json::Value as JsonValue; use uuid::Uuid; @@ -18,8 +17,6 @@ pub struct Collection { /// Local methods impl Collection { pub fn new(org_uuid: String, name: String) -> Self { - let now = Utc::now().naive_utc(); - Self { uuid: Uuid::new_v4().to_string(), @@ -46,8 +43,6 @@ use db::schema::collections; /// Database methods impl Collection { pub fn save(&mut self, conn: &DbConn) -> bool { - self.updated_at = Utc::now().naive_utc(); - match diesel::replace_into(collections::table) .values(&*self) .execute(&**conn) { diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 9539a65e..e3f1d53f 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -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) -> (String, i64) { // If there is no refresh token, we create one if self.refresh_token.is_empty() { use data_encoding::BASE64URL; @@ -51,9 +51,14 @@ impl Device { // Update the expiration of the device and the last update date let time_now = Utc::now().naive_utc(); - 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 use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER}; let claims = JWTClaims { @@ -61,16 +66,23 @@ impl Device { exp: (time_now + *DEFAULT_VALIDITY).timestamp(), iss: JWT_ISSUER.to_string(), sub: user.uuid.to_string(), + premium: true, name: user.name.to_string(), email: user.email.to_string(), email_verified: true, + + orgowner, + orgadmin, + orguser, + sstamp: user.security_stamp.to_string(), device: self.uuid.to_string(), scope: vec!["api".into(), "offline_access".into()], amr: vec!["Application".into()], }; + (encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds()) } } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index ed586ad2..4a9b7779 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -4,8 +4,16 @@ mod device; mod folder; mod user; +mod collection; +mod organization; + pub use self::attachment::Attachment; pub use self::cipher::Cipher; pub use self::device::Device; pub use self::folder::Folder; pub use self::user::User; + +pub use self::collection::Collection; +pub use self::organization::Organization; + +pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType}; diff --git a/src/db/models/org/organization.rs b/src/db/models/org/organization.rs deleted file mode 100644 index 22a2e0ca..00000000 --- a/src/db/models/org/organization.rs +++ /dev/null @@ -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 { - organizations::table - .filter(organizations::uuid.eq(uuid)) - .first::(&**conn).ok() - } -} diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs new file mode 100644 index 00000000..e4029a8b --- /dev/null +++ b/src/db/models/organization.rs @@ -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 { + organizations::table + .filter(organizations::uuid.eq(uuid)) + .first::(&**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 { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .load::(&**conn).expect("Error loading user organizations") + } + + pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .load::(&**conn).expect("Error loading user organizations") + } + + pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .first::(&**conn).ok() + } +} + + diff --git a/src/db/models/user.rs b/src/db/models/user.rs index dca0e1ed..cc9f428c 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -115,8 +115,21 @@ impl User { 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 = orgs.iter().map(|c| c.to_json(&conn)).collect(); - pub fn to_json(&self) -> JsonValue { json!({ "Id": self.uuid, "Name": self.name, @@ -129,19 +142,12 @@ impl User { "Key": self.key, "PrivateKey": self.private_key, "SecurityStamp": self.security_stamp, - "Organizations": [], + "Organizations": orgs_json, "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 { self.updated_at = Utc::now().naive_utc(); diff --git a/src/db/schema.rs b/src/db/schema.rs index 7bbdd2aa..e80d5b3d 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -95,9 +95,11 @@ table! { } table! { - users_organizations (user_uuid, org_uuid) { + users_organizations (uuid) { + uuid -> Text, user_uuid -> Text, org_uuid -> Text, + access_all -> Bool, key -> Text, status -> Integer, #[sql_name = "type"]