Skip to content

Commit 2191c0e

Browse files
committed
Add token-based crate ownership invitation acceptance
Adds an API endpoint, handler, and web route for token-based acceptance. Also updates crate ownership invitation email to contain a URL with a token for accepting an invitation.
1 parent 9dc37eb commit 2191c0e

File tree

7 files changed

+72
-8
lines changed

7 files changed

+72
-8
lines changed

app/router.js

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Router.map(function() {
4848
this.route('policies');
4949
this.route('data-access');
5050
this.route('confirm', { path: '/confirm/:email_token' });
51+
this.route('accept-invite', { path: '/accept-invite/:token' });
5152

5253
this.route('catch-all', { path: '*path' });
5354
});

app/routes/accept-invite.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Route from '@ember/routing/route';
2+
// import { inject as service } from '@ember/service';
3+
import ajax from 'ember-fetch/ajax';
4+
5+
export default Route.extend({
6+
// session: service(),
7+
8+
async model(params) {
9+
try {
10+
await ajax(`/api/v1//me/crate_owner_invitations/accept/${params.token}`, { method: 'PUT', body: '{}' });
11+
12+
// TODO: determine if user model reload is necessary here
13+
// The crate invitation acceptance draws heavily on prior art from the email confirmation, which
14+
// included the following block as a way to reload the user model after updating. It doesn't seem
15+
// to be necessary for this route, but it's unclear how best to verify that.
16+
// if (this.get('session.isLoggedIn')) {
17+
// ajax('/api/v1/me').then(response => {
18+
// this.session.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
19+
// });
20+
// }
21+
this.set('response', { accepted: true });
22+
return { response: this.get('response') };
23+
} catch (error) {
24+
this.set('response', { accepted: false });
25+
return { response: this.get('response') };
26+
}
27+
},
28+
});

app/templates/accept-invite.hbs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{#if model.response.accepted}}
2+
<h1>You've been added as a crate owner!</h1>
3+
<p>Visit your <a href="/dashboard">dashboard</a> to view all of your crates, or <a href="/me">account settings</a> to manage email notification preferences for all of your crates.</p>
4+
{{else}}
5+
<h1>Error in accepting crate ownership.</h1>
6+
<p>You may want to visit <a href="/me/pending-invites">crates.io/me/pending-invites</a> to try again.</p>
7+
{{/if}}

src/controllers/crate_owner_invitation.rs

+26-3
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,46 @@ pub fn handle_invite(req: &mut dyn Request) -> AppResult<Response> {
4141
serde_json::from_str(&body).map_err(|_| cargo_err("invalid json request"))?;
4242

4343
let crate_invite = crate_invite.crate_owner_invite;
44+
let user_id = req.user()?.id;
4445

4546
if crate_invite.accepted {
46-
accept_invite(req, conn, crate_invite)
47+
accept_invite(req, conn, crate_invite, user_id)
4748
} else {
4849
decline_invite(req, conn, crate_invite)
4950
}
5051
}
5152

53+
/// Handles the `PUT /me/crate_owner_invitations/accept/:token` route.
54+
pub fn handle_invite_with_token(req: &mut dyn Request) -> AppResult<Response> {
55+
let conn = req.db_conn()?;
56+
let req_token = &req.params()["token"];
57+
58+
let crate_owner_invite: CrateOwnerInvitation = crate_owner_invitations::table
59+
.filter(crate_owner_invitations::token.eq(req_token))
60+
.first::<CrateOwnerInvitation>(&*conn)?;
61+
62+
// TODO: Return 404 if no crate_owner_invite was found
63+
64+
let invite_reponse = InvitationResponse {
65+
crate_id: crate_owner_invite.crate_id,
66+
accepted: true,
67+
};
68+
accept_invite(
69+
req,
70+
&conn,
71+
invite_reponse,
72+
crate_owner_invite.invited_user_id,
73+
)
74+
}
75+
5276
fn accept_invite(
5377
req: &dyn Request,
5478
conn: &PgConnection,
5579
crate_invite: InvitationResponse,
80+
user_id: i32,
5681
) -> AppResult<Response> {
5782
use diesel::{delete, insert_into};
5883

59-
let user_id = req.user()?.id;
60-
6184
conn.transaction(|| {
6285
let pending_crate_owner = crate_owner_invitations::table
6386
.find((user_id, crate_invite.crate_id))

src/email.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ https://crates.io/confirm/{}",
9090
/// Whether or not the email is sent, the invitation entry will be created in
9191
/// the database and the user will see the invitation when they visit
9292
/// https://crates.io/me/pending-invites/.
93-
pub fn send_owner_invite_email(email: &str, user_name: &str, crate_name: &str) {
93+
pub fn send_owner_invite_email(email: &str, user_name: &str, crate_name: &str, token: &str) {
9494
let subject = "Crate ownership invitation";
9595
let body = format!(
9696
"{} has invited you to become an owner of the crate {}!\n
97-
Please visit https://crates.io/me/pending-invites to accept or reject
98-
this invitation.",
99-
user_name, crate_name
97+
Visit https://crates.io/accept-invite/{} to accept this invitation,
98+
or go to https://crates.io/me/pending-invites to manage all of your crate ownership invitations.",
99+
user_name, crate_name, token
100100
);
101101

102102
let _ = send_email(email, subject, &body);

src/models/krate.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,13 @@ impl Crate {
439439
.get_result::<CrateOwnerInvitation>(conn)
440440
.optional()?;
441441

442-
if maybe_inserted.is_some() {
442+
if let Some(ownership_invitation) = maybe_inserted {
443443
if let Ok(Some(email)) = user.verified_email(&conn) {
444444
email::send_owner_invite_email(
445445
&email.as_str(),
446446
&req_user.gh_login.as_str(),
447447
&self.name.as_str(),
448+
&ownership_invitation.token.as_str(),
448449
);
449450
}
450451
}

src/router.rs

+4
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ pub fn build_router(app: &App) -> R404 {
8989
"/me/crate_owner_invitations/:crate_id",
9090
C(crate_owner_invitation::handle_invite),
9191
);
92+
api_router.put(
93+
"/me/crate_owner_invitations/accept/:token",
94+
C(crate_owner_invitation::handle_invite_with_token),
95+
);
9296
api_router.put(
9397
"/me/email_notifications",
9498
C(user::me::update_email_notifications),

0 commit comments

Comments
 (0)