Skip to content

Commit 8f4a4f5

Browse files
committed
Auto merge of #1955 - nkanderson:1360_invitation_token, r=carols10cents
1360 crate ownership invitation token Completes #1360
2 parents 116d13f + b12465e commit 8f4a4f5

File tree

12 files changed

+91
-9
lines changed

12 files changed

+91
-9
lines changed

app/router.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 15 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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}}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TRIGGER trigger_crate_owner_invitations_set_token_generated_at ON crate_owner_invitations;
2+
DROP FUNCTION crate_owner_invitations_set_token_generated_at();
3+
ALTER TABLE crate_owner_invitations DROP COLUMN token_generated_at;
4+
ALTER TABLE crate_owner_invitations DROP COLUMN token;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
ALTER TABLE crate_owner_invitations ADD COLUMN token TEXT NOT NULL DEFAULT random_string(26);
2+
ALTER TABLE crate_owner_invitations ADD COLUMN token_generated_at TIMESTAMP;
3+
4+
CREATE FUNCTION crate_owner_invitations_set_token_generated_at() RETURNS trigger AS $$
5+
BEGIN
6+
NEW.token_generated_at := CURRENT_TIMESTAMP;
7+
RETURN NEW;
8+
END
9+
$$ LANGUAGE plpgsql;
10+
11+
CREATE TRIGGER trigger_crate_owner_invitations_set_token_generated_at BEFORE
12+
INSERT OR UPDATE OF token ON crate_owner_invitations
13+
FOR EACH ROW EXECUTE PROCEDURE crate_owner_invitations_set_token_generated_at();

src/controllers/crate_owner_invitation.rs

Lines changed: 24 additions & 3 deletions
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

Lines changed: 4 additions & 4 deletions
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/crate_owner_invitation.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ use crate::schema::{crate_owner_invitations, crates, users};
55
use crate::views::EncodableCrateOwnerInvitation;
66

77
/// The model representing a row in the `crate_owner_invitations` database table.
8-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Identifiable, Queryable)]
8+
#[derive(Clone, Debug, PartialEq, Eq, Identifiable, Queryable)]
99
#[primary_key(invited_user_id, crate_id)]
1010
pub struct CrateOwnerInvitation {
1111
pub invited_user_id: i32,
1212
pub invited_by_user_id: i32,
1313
pub crate_id: i32,
1414
pub created_at: NaiveDateTime,
15+
pub token: String,
16+
pub token_created_at: Option<NaiveDateTime>,
1517
}
1618

1719
#[derive(Insertable, Clone, Copy, Debug)]

src/models/krate.rs

Lines changed: 2 additions & 1 deletion
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ pub fn build_router(app: &App) -> R404 {
8888
"/me/crate_owner_invitations/:crate_id",
8989
C(crate_owner_invitation::handle_invite),
9090
);
91+
api_router.put(
92+
"/me/crate_owner_invitations/accept/:token",
93+
C(crate_owner_invitation::handle_invite_with_token),
94+
);
9195
api_router.put(
9296
"/me/email_notifications",
9397
C(user::me::update_email_notifications),

src/schema.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ table! {
208208
///
209209
/// (Automatically generated by Diesel.)
210210
created_at -> Timestamp,
211+
/// The `token` column of the `crate_owner_invitations` table.
212+
///
213+
/// Its SQL type is `Text`.
214+
///
215+
/// (Automatically generated by Diesel.)
216+
token -> Text,
217+
/// The `token_generated_at` column of the `crate_owner_invitations` table.
218+
///
219+
/// Its SQL type is `Nullable<Timestamp>`.
220+
///
221+
/// (Automatically generated by Diesel.)
222+
token_generated_at -> Nullable<Timestamp>,
211223
}
212224
}
213225

src/tasks/dump_db/dump-db.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ invited_user_id = "private"
5656
invited_by_user_id = "private"
5757
crate_id = "private"
5858
created_at = "private"
59+
token = "private"
60+
token_generated_at = "private"
5961

6062
[crate_owners]
6163
dependencies = ["crates", "users"]

0 commit comments

Comments
 (0)