Skip to content

Accept owner invites #1073

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

Merged
merged 16 commits into from
Sep 25, 2017
Merged
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
27 changes: 27 additions & 0 deletions app/components/pending-owner-invite-row.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Ember from 'ember';

export default Ember.Component.extend({
isSuccess: false,
isError: false,
inviteError: 'default error message',

actions: {
acceptInvitation(invite) {
invite.set('accepted', true);
invite.save()
.then(() => {
this.set('isSuccess', true);
})
.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');
}
});
}
}
});
3 changes: 2 additions & 1 deletion app/models/crate-owner-invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export default DS.Model.extend({
invited_by_username: DS.attr('string'),
crate_name: DS.attr('string'),
crate_id: DS.attr('number'),
created_at: DS.attr('date')
created_at: DS.attr('date'),
accepted: DS.attr('boolean', { defaultValue: false })
});
3 changes: 3 additions & 0 deletions app/serializers/crate-owner-invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export default DS.RESTSerializer.extend({
primaryKey: 'crate_id',
modelNameFromPayloadKey() {
return 'crate-owner-invite';
},
payloadKeyFromModelName() {
return 'crate_owner_invite';
}
});
8 changes: 8 additions & 0 deletions app/styles/me.scss
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@
@include justify-content(space-between);

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

Expand Down
36 changes: 36 additions & 0 deletions app/templates/components/pending-owner-invite-row.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class='row'>
{{#if isSuccess }}
<p>Success! You've been added as an owner of crate
{{#link-to 'crate' invite.crate_name}}{{invite.crate_name}}{{/link-to}}.
</p>
{{else}}
<div class='info'>
<div class='name'>
<h3>
{{#link-to 'crate' invite.crate_name}}
{{invite.crate_name}}
{{/link-to}}
</h3>
</div>
<div class='invite'>
<p>Invited by:
{{#link-to 'user' invite.invited_by_username}}
{{invite.invited_by_username}}
{{/link-to}}
</p>
</div>
<div class='sent'>
<span class='small'>{{moment-from-now invite.created_at}}</span>
</div>
<div class='actions'>
<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>
{{/if}}
</div>
28 changes: 3 additions & 25 deletions app/templates/me/pending-invites.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,9 @@
<div id='my-invites'>
<div class='white-rows'>
{{#each model as |invite|}}
<div class='row'>
<div class='info'>
<div class='name'>
<h3>
{{#link-to 'crate' invite.crate_name}}
{{invite.crate_name}}
{{/link-to}}
</h3>
</div>
<div class='invite'>
<p>Invited by:
{{#link-to 'user' invite.invited_by_username}}
{{invite.invited_by_username}}
{{/link-to}}
</p>
</div>
<div class='sent'>
<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'>Deny</button>
</div>
</div>
</div>
{{pending-owner-invite-row invite=invite}}
{{else}}
<p>You don't seem to have any pending invitations.</p>
{{/each}}
</div>
</div>
78 changes: 76 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,75 @@ pub fn list(req: &mut Request) -> CargoResult<Response> {
}
Ok(req.json(&R { crate_owner_invitations }))
}

#[derive(Deserialize)]
struct OwnerInvitation {
crate_owner_invite: InvitationResponse,
}

#[derive(Deserialize, Serialize, Debug, Copy, Clone)]
pub struct InvitationResponse {
pub crate_id: i32,
pub accepted: bool,
}

/// Handles the `PUT /me/crate_owner_invitations/:crate_id` route.
pub fn handle_invite(req: &mut Request) -> CargoResult<Response> {

let conn = &*req.db_conn()?;


let mut body = String::new();
req.body().read_to_string(&mut body)?;

let crate_invite: OwnerInvitation = serde_json::from_str(&body).map_err(|_| {
human("invalid json request")
})?;

let crate_invite = crate_invite.crate_owner_invite;

if crate_invite.accepted {
accept_invite(req, conn, crate_invite)
} else {
#[derive(Serialize)]
struct R {
crate_owner_invitation: InvitationResponse,
}
Ok(req.json(&R { crate_owner_invitation: crate_invite }))
}
}

fn accept_invite(
req: &mut Request,
conn: &PgConnection,
crate_invite: InvitationResponse,
) -> CargoResult<Response> {
let user_id = req.user()?.id;
use diesel::{insert, delete};
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))
.filter(crate_owner_invitations::invited_user_id.eq(user_id)),
).execute(conn)?;

#[derive(Serialize)]
struct R {
crate_owner_invitation: InvitationResponse,
}
Ok(req.json(&R { crate_owner_invitation: crate_invite }))
})
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ pub fn middleware(app: Arc<App>) -> MiddlewareBuilder {
"/me/crate_owner_invitations",
C(crate_owner_invitation::list),
);
api_router.put(
"/me/crate_owner_invitations/:crate_id",
C(crate_owner_invitation::handle_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
99 changes: 98 additions & 1 deletion src/tests/owners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use {CrateList, GoodCrate};
use cargo_registry::owner::EncodableOwner;
use cargo_registry::user::EncodablePublicUser;
use cargo_registry::crate_owner_invitation::{EncodableCrateOwnerInvitation,
NewCrateOwnerInvitation};
NewCrateOwnerInvitation, InvitationResponse};
use cargo_registry::schema::crate_owner_invitations;

use conduit::{Handler, Method};
Expand Down Expand Up @@ -370,3 +370,100 @@ 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 R {
crate_owner_invitations: Vec<EncodableCrateOwnerInvitation>,
}

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

#[derive(Deserialize)]
struct T {
crate_owner_invitation: InvitationResponse,
}

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_invite": {
"invited_by_username": "inviting_user",
"crate_name": "invited_crate",
"crate_id": krate.id,
"created_at": "",
"accepted": true
}
});

// first check that response from inserting new crate owner
// and deleting crate_owner_invitation is okay
let mut response = ok_resp!(
middle.call(
req.with_path(&format!("api/v1/me/crate_owner_invitations/{}", krate.id))
.with_method(Method::Put)
.with_body(body.to_string().as_bytes()),
)
);

let json: T = ::json(&mut response);
assert_eq!(json.crate_owner_invitation.accepted, true);
assert_eq!(json.crate_owner_invitation.crate_id, krate.id);

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