Skip to content

Commit c14a03e

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 ab2c4bb commit c14a03e

File tree

7 files changed

+57
-8
lines changed

7 files changed

+57
-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

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Route from '@ember/routing/route';
2+
import ajax from 'ember-fetch/ajax';
3+
4+
export default Route.extend({
5+
async model(params) {
6+
try {
7+
await ajax(`/api/v1//me/crate_owner_invitations/accept/${params.token}`, { method: 'PUT', body: '{}' });
8+
this.set('response', { accepted: true });
9+
return { response: this.get('response') };
10+
} catch (error) {
11+
this.set('response', { accepted: false });
12+
return { response: this.get('response') };
13+
}
14+
},
15+
});

app/templates/accept-invite.hbs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{#if this.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

+24-3
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,44 @@ pub fn handle_invite(req: &mut dyn Request) -> AppResult<Response> {
4141
serde_json::from_str(&body).map_err(|_| bad_request("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+
let invite_reponse = InvitationResponse {
63+
crate_id: crate_owner_invite.crate_id,
64+
accepted: true,
65+
};
66+
accept_invite(
67+
req,
68+
&conn,
69+
invite_reponse,
70+
crate_owner_invite.invited_user_id,
71+
)
72+
}
73+
5274
fn accept_invite(
5375
req: &dyn Request,
5476
conn: &PgConnection,
5577
crate_invite: InvitationResponse,
78+
user_id: i32,
5679
) -> AppResult<Response> {
5780
use diesel::{delete, insert_into};
5881

59-
let user_id = req.user()?.id;
60-
6182
conn.transaction(|| {
6283
let pending_crate_owner = crate_owner_invitations::table
6384
.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
@@ -440,12 +440,13 @@ impl Crate {
440440
.get_result::<CrateOwnerInvitation>(conn)
441441
.optional()?;
442442

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

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)