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}}
+
+ {{/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);
+}