Skip to content

1360 crate ownership invitation token #1955

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 3 commits into from
Jan 15, 2020
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
1 change: 1 addition & 0 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Router.map(function() {
this.route('policies');
this.route('data-access');
this.route('confirm', { path: '/confirm/:email_token' });
this.route('accept-invite', { path: '/accept-invite/:token' });

this.route('catch-all', { path: '*path' });
});
Expand Down
15 changes: 15 additions & 0 deletions app/routes/accept-invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Route from '@ember/routing/route';
import ajax from 'ember-fetch/ajax';

export default Route.extend({
async model(params) {
try {
await ajax(`/api/v1/me/crate_owner_invitations/accept/${params.token}`, { method: 'PUT', body: '{}' });
this.set('response', { accepted: true });
return { response: this.get('response') };
} catch (error) {
this.set('response', { accepted: false });
return { response: this.get('response') };
}
},
});
7 changes: 7 additions & 0 deletions app/templates/accept-invite.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#if this.model.response.accepted}}
<h1>You've been added as a crate owner!</h1>
<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>
{{else}}
<h1>Error in accepting crate ownership.</h1>
<p>You may want to visit <a href="/me/pending-invites">crates.io/me/pending-invites</a> to try again.</p>
{{/if}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP TRIGGER trigger_crate_owner_invitations_set_token_generated_at ON crate_owner_invitations;
DROP FUNCTION crate_owner_invitations_set_token_generated_at();
ALTER TABLE crate_owner_invitations DROP COLUMN token_generated_at;
ALTER TABLE crate_owner_invitations DROP COLUMN token;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ALTER TABLE crate_owner_invitations ADD COLUMN token TEXT NOT NULL DEFAULT random_string(26);
ALTER TABLE crate_owner_invitations ADD COLUMN token_generated_at TIMESTAMP;

CREATE FUNCTION crate_owner_invitations_set_token_generated_at() RETURNS trigger AS $$
BEGIN
NEW.token_generated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_crate_owner_invitations_set_token_generated_at BEFORE
INSERT OR UPDATE OF token ON crate_owner_invitations
FOR EACH ROW EXECUTE PROCEDURE crate_owner_invitations_set_token_generated_at();
27 changes: 24 additions & 3 deletions src/controllers/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,44 @@ pub fn handle_invite(req: &mut dyn Request) -> AppResult<Response> {
serde_json::from_str(&body).map_err(|_| bad_request("invalid json request"))?;

let crate_invite = crate_invite.crate_owner_invite;
let user_id = req.user()?.id;

if crate_invite.accepted {
accept_invite(req, conn, crate_invite)
accept_invite(req, conn, crate_invite, user_id)
} else {
decline_invite(req, conn, crate_invite)
}
}

/// Handles the `PUT /me/crate_owner_invitations/accept/:token` route.
pub fn handle_invite_with_token(req: &mut dyn Request) -> AppResult<Response> {
let conn = req.db_conn()?;
let req_token = &req.params()["token"];

let crate_owner_invite: CrateOwnerInvitation = crate_owner_invitations::table
.filter(crate_owner_invitations::token.eq(req_token))
.first::<CrateOwnerInvitation>(&*conn)?;

let invite_reponse = InvitationResponse {
crate_id: crate_owner_invite.crate_id,
accepted: true,
};
accept_invite(
req,
&conn,
invite_reponse,
crate_owner_invite.invited_user_id,
)
}

fn accept_invite(
req: &dyn Request,
conn: &PgConnection,
crate_invite: InvitationResponse,
user_id: i32,
) -> AppResult<Response> {
use diesel::{delete, insert_into};

let user_id = req.user()?.id;

conn.transaction(|| {
let pending_crate_owner = crate_owner_invitations::table
.find((user_id, crate_invite.crate_id))
Expand Down
8 changes: 4 additions & 4 deletions src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ https://crates.io/confirm/{}",
/// Whether or not the email is sent, the invitation entry will be created in
/// the database and the user will see the invitation when they visit
/// https://crates.io/me/pending-invites/.
pub fn send_owner_invite_email(email: &str, user_name: &str, crate_name: &str) {
pub fn send_owner_invite_email(email: &str, user_name: &str, crate_name: &str, token: &str) {
let subject = "Crate ownership invitation";
let body = format!(
"{} has invited you to become an owner of the crate {}!\n
Please visit https://crates.io/me/pending-invites to accept or reject
this invitation.",
user_name, crate_name
Visit https://crates.io/accept-invite/{} to accept this invitation,
or go to https://crates.io/me/pending-invites to manage all of your crate ownership invitations.",
user_name, crate_name, token
);

let _ = send_email(email, subject, &body);
Expand Down
4 changes: 3 additions & 1 deletion src/models/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ use crate::schema::{crate_owner_invitations, crates, users};
use crate::views::EncodableCrateOwnerInvitation;

/// The model representing a row in the `crate_owner_invitations` database table.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Identifiable, Queryable)]
#[derive(Clone, Debug, PartialEq, Eq, Identifiable, Queryable)]
#[primary_key(invited_user_id, crate_id)]
pub struct CrateOwnerInvitation {
pub invited_user_id: i32,
pub invited_by_user_id: i32,
pub crate_id: i32,
pub created_at: NaiveDateTime,
pub token: String,
pub token_created_at: Option<NaiveDateTime>,
}

#[derive(Insertable, Clone, Copy, Debug)]
Expand Down
3 changes: 2 additions & 1 deletion src/models/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,12 +440,13 @@ impl Crate {
.get_result::<CrateOwnerInvitation>(conn)
.optional()?;

if maybe_inserted.is_some() {
if let Some(ownership_invitation) = maybe_inserted {
if let Ok(Some(email)) = user.verified_email(&conn) {
email::send_owner_invite_email(
&email.as_str(),
&req_user.gh_login.as_str(),
&self.name.as_str(),
&ownership_invitation.token.as_str(),
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ pub fn build_router(app: &App) -> R404 {
"/me/crate_owner_invitations/:crate_id",
C(crate_owner_invitation::handle_invite),
);
api_router.put(
"/me/crate_owner_invitations/accept/:token",
C(crate_owner_invitation::handle_invite_with_token),
);
api_router.put(
"/me/email_notifications",
C(user::me::update_email_notifications),
Expand Down
12 changes: 12 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ table! {
///
/// (Automatically generated by Diesel.)
created_at -> Timestamp,
/// The `token` column of the `crate_owner_invitations` table.
///
/// Its SQL type is `Text`.
///
/// (Automatically generated by Diesel.)
token -> Text,
/// The `token_generated_at` column of the `crate_owner_invitations` table.
///
/// Its SQL type is `Nullable<Timestamp>`.
///
/// (Automatically generated by Diesel.)
token_generated_at -> Nullable<Timestamp>,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/tasks/dump_db/dump-db.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ invited_user_id = "private"
invited_by_user_id = "private"
crate_id = "private"
created_at = "private"
token = "private"
token_generated_at = "private"

[crate_owners]
dependencies = ["crates", "users"]
Expand Down