diff --git a/app/controllers/me/pending-invites.js b/app/controllers/me/pending-invites.js new file mode 100644 index 00000000000..08a547ba3c7 --- /dev/null +++ b/app/controllers/me/pending-invites.js @@ -0,0 +1,33 @@ +import Ember from 'ember'; +import { inject as service } from '@ember/service'; + +export default Ember.Controller.extend({ + ajax: service(), + isError: false, + inviteError: 'default error message', + + actions: { + acceptInvitation(invite) { + this.get('ajax').put('/api/v1/me/accept_owner_invite', { + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({ + crate_owner_invitation: { + invited_by_username: invite.get('invited_by_username'), + crate_name: invite.get('crate_name'), + crate_id: invite.get('crate_id'), + created_at: invite.get('created_at') + } + }) + }).catch((error) => { + this.set('isError', true); + if (error.payload) { + this.set('inviteError', + `Error in accepting invite: ${error.payload.errors[0].detail}` + ); + } else { + this.set('inviteError', 'Error in accepting invite'); + } + }); + } + } +}); diff --git a/app/styles/me.scss b/app/styles/me.scss index c5e6a5fb113..2e22e08c5ab 100644 --- a/app/styles/me.scss +++ b/app/styles/me.scss @@ -185,6 +185,11 @@ @include justify-content(space-between); .date { @include flex-grow(2); text-align: right; } + .label { + .small-text { + font-size: 90%; + } + } } } diff --git a/app/templates/me/pending-invites.hbs b/app/templates/me/pending-invites.hbs index df6f0e8e985..0b5ff1ab379 100644 --- a/app/templates/me/pending-invites.hbs +++ b/app/templates/me/pending-invites.hbs @@ -28,11 +28,18 @@ {{moment-from-now invite.created_at}}
- +
+ {{#if isError}} +
+

{{inviteError}}

+
+ {{/if}} + {{else}} +

You don't seem to have any pending invitations.

{{/each}} diff --git a/src/crate_owner_invitation.rs b/src/crate_owner_invitation.rs index 085fa84f3ec..6f2693a3b58 100644 --- a/src/crate_owner_invitation.rs +++ b/src/crate_owner_invitation.rs @@ -1,12 +1,14 @@ use conduit::{Request, Response}; use diesel::prelude::*; use time::Timespec; +use serde_json; use db::RequestTransaction; -use schema::{crate_owner_invitations, users, crates}; +use schema::{crate_owner_invitations, users, crates, crate_owners}; use user::RequestUser; -use util::errors::CargoResult; +use util::errors::{CargoResult, human}; use util::RequestUtils; +use owner::{CrateOwner, OwnerKind}; /// The model representing a row in the `crate_owner_invitations` database table. #[derive(Clone, Copy, Debug, PartialEq, Eq, Identifiable, Queryable)] @@ -80,3 +82,48 @@ pub fn list(req: &mut Request) -> CargoResult { } Ok(req.json(&R { crate_owner_invitations })) } + +/// Handles the `PUT /me/accept_owner_invite` route. +pub fn accept_invite(req: &mut Request) -> CargoResult { + use diesel::{insert, delete}; + let conn = &*req.db_conn()?; + let user_id = req.user()?.id; + + let mut body = String::new(); + req.body().read_to_string(&mut body)?; + + #[derive(Deserialize)] + struct OwnerInvitation { + crate_owner_invitation: EncodableCrateOwnerInvitation, + } + + let crate_invite: OwnerInvitation = serde_json::from_str(&body).map_err( + |_| human("invalid json request"), + )?; + + let crate_invite = crate_invite.crate_owner_invitation; + + let pending_crate_owner = crate_owner_invitations::table + .filter(crate_owner_invitations::crate_id.eq(crate_invite.crate_id)) + .filter(crate_owner_invitations::invited_user_id.eq(user_id)) + .first::(&*conn)?; + + let owner = CrateOwner { + crate_id: crate_invite.crate_id, + owner_id: user_id, + created_by: pending_crate_owner.invited_by_user_id, + owner_kind: OwnerKind::User as i32, + }; + + conn.transaction(|| { + insert(&owner).into(crate_owners::table).execute(conn)?; + delete(crate_owner_invitations::table.filter(crate_owner_invitations::crate_id.eq(crate_invite.crate_id))) + .execute(conn)?; + + #[derive(Serialize)] + struct R { + ok: bool, + } + Ok(req.json(&R { ok: true })) + }) +} diff --git a/src/lib.rs b/src/lib.rs index 442ed59d7ec..944cc6eb71b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { "/me/crate_owner_invitations", C(crate_owner_invitation::list), ); + api_router.put("/me/accept_owner_invite", C(crate_owner_invitation::accept_invite)); api_router.get("/summary", C(krate::summary)); api_router.put("/confirm/:email_token", C(user::confirm_user_email)); api_router.put("/users/:user_id/resend", C(user::regenerate_token_and_send)); diff --git a/src/tests/all.rs b/src/tests/all.rs index 633ec9533b0..caf07d7ef16 100644 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -15,6 +15,7 @@ extern crate dotenv; extern crate git2; extern crate semver; extern crate serde; +#[macro_use] extern crate serde_json; extern crate time; extern crate url; diff --git a/src/tests/http-data/owners_test_accept_invitation b/src/tests/http-data/owners_test_accept_invitation new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/src/tests/http-data/owners_test_accept_invitation @@ -0,0 +1 @@ +[] diff --git a/src/tests/owners.rs b/src/tests/owners.rs index b19c59fae50..7c7ddf58818 100644 --- a/src/tests/owners.rs +++ b/src/tests/owners.rs @@ -370,3 +370,97 @@ fn invitations_list() { assert_eq!(json.crate_owner_invitations[0].crate_name, "invited_crate"); assert_eq!(json.crate_owner_invitations[0].crate_id, krate.id); } + +/* Given a user inviting a different user to be a crate + owner, check that the user invited can accept their + invitation, the invitation will be deleted from + the invitations table, and a new crate owner will be + inserted into the table for the given crate. +*/ +#[test] +fn test_accept_invitation() { + #[derive(Deserialize)] + struct S { + ok: bool + } + + #[derive(Deserialize)] + struct R { + crate_owner_invitations: Vec, + } + + #[derive(Deserialize)] + struct Q { + users: Vec, + } + + let (_b, app, middle) = ::app(); + let mut req = ::req( + app.clone(), + Method::Get, + "/api/v1/me/crate_owner_invitations", + ); + let (krate, user) = { + let conn = app.diesel_database.get().unwrap(); + let owner = ::new_user("inviting_user").create_or_update(&conn).unwrap(); + let user = ::new_user("invited_user").create_or_update(&conn).unwrap(); + let krate = ::CrateBuilder::new("invited_crate", owner.id).expect_build(&conn); + + // This should be replaced by an actual call to the route that `owner --add` hits once + // that route creates an invitation. + let invitation = NewCrateOwnerInvitation { + invited_by_user_id: owner.id, + invited_user_id: user.id, + crate_id: krate.id, + }; + diesel::insert(&invitation) + .into(crate_owner_invitations::table) + .execute(&*conn) + .unwrap(); + (krate, user) + }; + ::sign_in_as(&mut req, &user); + + let body = json!({ + "crate_owner_invitation": { + "invited_by_username": "inviting_user", + "crate_name": "invited_crate", + "crate_id": krate.id, + "created_at": "" + } + }); + + // first check that response from inserting new crate owner + // and deleting crate_owner_inviitation is okay + let mut response = ok_resp!( + middle.call( + req.with_path("api/v1/me/accept_owner_invite") + .with_method(Method::Put) + .with_body(body.to_string().as_bytes()), + ) + ); + + assert!(::json::(&mut response).ok); + + // then check to make sure that accept_invite did what it + // was supposed to + // crate_owner_invitation was deleted + let mut response = ok_resp!( + middle.call( + req.with_path("api/v1/me/crate_owner_invitations") + .with_method(Method::Get) + ) + ); + let json: R = ::json(&mut response); + assert_eq!(json.crate_owner_invitations.len(), 0); + + // new crate owner was inserted + let mut response = ok_resp!( + middle.call( + req.with_path("/api/v1/crates/invited_crate/owners") + .with_method(Method::Get) + ) + ); + let json: Q = ::json(&mut response); + assert_eq!(json.users.len(), 2); +}