Skip to content

Accept owner invites - help with Ember #1066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/controllers/me/pending-invites.js
Original file line number Diff line number Diff line change
@@ -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');
}
});
}
}
});
5 changes: 5 additions & 0 deletions app/styles/me.scss
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@
@include justify-content(space-between);

.date { @include flex-grow(2); text-align: right; }
.label {
.small-text {
font-size: 90%;
}
}
}
}

Expand Down
9 changes: 8 additions & 1 deletion app/templates/me/pending-invites.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@
<span class='small'>{{moment-from-now invite.created_at}}</span>
</div>
<div class='actions'>
<button class='small yellow-button'>Accept</button>
<button class='small yellow-button' {{action 'acceptInvitation' invite}}>Accept</button>
<button class='small yellow-button'>Deny</button>
</div>
{{#if isError}}
<div class='label'>
<p class='small-text'>{{inviteError}}</p>
</div>
{{/if}}
</div>
</div>
{{else}}
<p>You don't seem to have any pending invitations.</p>
{{/each}}
</div>
</div>
51 changes: 49 additions & 2 deletions src/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -80,3 +82,48 @@ pub fn list(req: &mut Request) -> CargoResult<Response> {
}
Ok(req.json(&R { crate_owner_invitations }))
}

/// Handles the `PUT /me/accept_owner_invite` route.
pub fn accept_invite(req: &mut Request) -> CargoResult<Response> {
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::<CrateOwnerInvitation>(&*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 }))
})
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ pub fn middleware(app: Arc<App>) -> 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));
Expand Down
1 change: 1 addition & 0 deletions src/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/tests/http-data/owners_test_accept_invitation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
94 changes: 94 additions & 0 deletions src/tests/owners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncodableCrateOwnerInvitation>,
}

#[derive(Deserialize)]
struct Q {
users: Vec<EncodablePublicUser>,
}

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::<S>(&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);
}