diff --git a/app/components/api-token-row.js b/app/components/api-token-row.js
index 280d1abad60..686c10ada0e 100644
--- a/app/components/api-token-row.js
+++ b/app/components/api-token-row.js
@@ -1,10 +1,13 @@
import Component from '@ember/component';
+import { inject as service } from '@ember/service';
import { empty, or } from '@ember/object/computed';
export default Component.extend({
emptyName: empty('api_token.name'),
disableCreate: or('api_token.isSaving', 'emptyName'),
serverError: null,
+ session: service(),
+ store: service(),
didInsertElement() {
let input = this.element.querySelector('input');
@@ -17,6 +20,7 @@ export default Component.extend({
async saveToken() {
try {
await this.api_token.save();
+ this.set('session.currentUser.has_tokens', true);
this.set('serverError', null);
} catch (err) {
let msg;
@@ -31,6 +35,11 @@ export default Component.extend({
async revokeToken() {
try {
+ // To avoid error on destroy we need to set before destroying of api-token
+ // that's why we need to set length of api-tokens to 1 in check
+ if ((await this.store.query('api-token', {})).length == 1) {
+ this.set('session.currentUser.has_tokens', false);
+ }
await this.api_token.destroyRecord();
} catch (err) {
let msg;
diff --git a/app/components/welcome-message.hbs b/app/components/welcome-message.hbs
new file mode 100644
index 00000000000..8c91120f20a
--- /dev/null
+++ b/app/components/welcome-message.hbs
@@ -0,0 +1,3 @@
+
+ Welcome to crates.io! Visit account settings to {{this.text}}
+
\ No newline at end of file
diff --git a/app/components/welcome-message.js b/app/components/welcome-message.js
new file mode 100644
index 00000000000..fd8f179bd7f
--- /dev/null
+++ b/app/components/welcome-message.js
@@ -0,0 +1,26 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { computed } from '@ember/object';
+import { notEmpty } from '@ember/object/computed';
+
+export default Component.extend({
+ session: service(),
+
+ text: computed('session.currentUser.{email_verified,has_tokens}', function() {
+ const user = this.get('session.currentUser');
+ if (!user || (user.email_verified && user.has_tokens)) return '';
+
+ const textArray = [
+ !user.email_verified && 'verify your email address',
+ !user.email_verified && !user.has_tokens && ' and ',
+ !user.has_tokens && 'create an API token',
+ '!',
+ ].filter(e => !!e);
+
+ return textArray.join('');
+ }),
+
+ showMessage: notEmpty('text').readOnly(),
+
+ tagName: '',
+});
diff --git a/app/components/welcome-message.module.scss b/app/components/welcome-message.module.scss
new file mode 100644
index 00000000000..6c7dce346ab
--- /dev/null
+++ b/app/components/welcome-message.module.scss
@@ -0,0 +1,18 @@
+.welcome-message {
+ display: none;
+ font-weight: bold;
+ font-size: 110%;
+ text-align: center;
+ margin: 0 0 10px 0;
+ padding: 10px;
+ border-radius: 5px;
+
+ &.shown {
+ display: block;
+ }
+
+ &.info {
+ background-color: $main-bg-dark;
+ border: 2px solid #62865f;
+ }
+}
diff --git a/app/models/user.js b/app/models/user.js
index 7fa7fd1f8f8..e991b53e223 100644
--- a/app/models/user.js
+++ b/app/models/user.js
@@ -9,6 +9,7 @@ export default Model.extend({
avatar: attr('string'),
url: attr('string'),
kind: attr('string'),
+ has_tokens: attr('boolean'),
stats() {
return this.store.adapterFor('user').stats(this.id);
diff --git a/app/routes/me/index.js b/app/routes/me/index.js
index b35a921b122..11794e29ffc 100644
--- a/app/routes/me/index.js
+++ b/app/routes/me/index.js
@@ -5,12 +5,10 @@ import AuthenticatedRoute from '../../mixins/authenticated-route';
export default Route.extend(AuthenticatedRoute, {
actions: {
willTransition: function() {
- this.controller
- .setProperties({
- emailNotificationsSuccess: false,
- emailNotificationsError: false,
- })
- .clear();
+ this.controller.setProperties({
+ emailNotificationsSuccess: false,
+ emailNotificationsError: false,
+ });
},
},
model() {
diff --git a/app/services/flash-messages.js b/app/services/flash-messages.js
index 21ea24e08c5..ed00ae4edc0 100644
--- a/app/services/flash-messages.js
+++ b/app/services/flash-messages.js
@@ -4,8 +4,9 @@ export default class FlashMessagesService extends Service {
message = null;
_nextMessage = null;
- show(message) {
+ show(message, options = { type: 'warning' }) {
this.set('message', message);
+ this.set('options', options);
}
queue(message) {
diff --git a/app/templates/catch-all.hbs b/app/templates/catch-all.hbs
index 41ff2a09f10..b918f0251b8 100644
--- a/app/templates/catch-all.hbs
+++ b/app/templates/catch-all.hbs
@@ -3,13 +3,6 @@
Perhaps a search of the site may help?
-
-
+
+
\ No newline at end of file
diff --git a/app/templates/index.hbs b/app/templates/index.hbs
index 207840e9c05..a7485afb30a 100644
--- a/app/templates/index.hbs
+++ b/app/templates/index.hbs
@@ -1,8 +1,10 @@
+
The Rust community’s crate registry
-
+
{{svg-jar "button-download"}}
Install Cargo
@@ -24,12 +26,14 @@
{{svg-jar "download"}}
- {{if this.hasData (format-num this.model.num_downloads) "---,---,---"}}
+ {{if this.hasData (format-num this.model.num_downloads) "---,---,---"}}
Downloads
{{svg-jar "crate"}}
- {{if this.hasData (format-num this.model.num_crates) "---,---"}}
+ {{if this.hasData (format-num this.model.num_crates) "---,---"}}
Crates in stock
@@ -53,11 +57,13 @@
- Popular Keywords (see all)
+ Popular Keywords (see all)
+
- Popular Categories (see all)
+ Popular Categories (see all)
+
-
+
\ No newline at end of file
diff --git a/mirage/factories/user.js b/mirage/factories/user.js
index 66ed4e55f74..d494b1dde31 100644
--- a/mirage/factories/user.js
+++ b/mirage/factories/user.js
@@ -1,16 +1,29 @@
-import { Factory } from 'ember-cli-mirage';
-import { dasherize } from '@ember/string';
+import { Factory, trait } from 'ember-cli-mirage';
+import faker from 'faker';
export default Factory.extend({
- name: i => `User ${i + 1}`,
-
+ email_verified: false,
+ email_verification_sent: true,
+ name() {
+ return faker.name.findName();
+ },
login() {
- return dasherize(this.name);
+ return faker.internet.userName();
+ },
+ avatar() {
+ return faker.image.imageUrl();
},
-
url() {
- return `https://github.com/${this.login}`;
+ return faker.internet.url();
},
+ kind: 'user',
+ has_tokens: false,
+
+ withVerifiedEmail: trait({
+ email_verified: true,
+ }),
- avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
+ withTokens: trait({
+ has_tokens: true,
+ }),
});
diff --git a/package-lock.json b/package-lock.json
index 5a6a853fdd7..faa9fec7ab1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38923,6 +38923,12 @@
"integrity": "sha512-Kn2WYYS6cDBS5jq/voOfSGCA0TafOYAUPbEp8mUVpD/DVV5bQIDjlq+MLLvNUokkbTpjBVlLDaM5PnX+PwZMlw==",
"dev": true
},
+ "faker": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz",
+ "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=",
+ "dev": true
+ },
"fast-deep-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
diff --git a/package.json b/package.json
index 0860e31d1b2..eedc536af5e 100644
--- a/package.json
+++ b/package.json
@@ -93,6 +93,7 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-ember": "^7.10.1",
"eslint-plugin-prettier": "^3.1.2",
+ "faker": "^4.1.0",
"loader.js": "^4.7.0",
"normalize.css": "^8.0.1",
"nyc": "^15.0.0",
diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs
index dfd1fc1c6b6..07de33b8872 100644
--- a/src/controllers/user/me.rs
+++ b/src/controllers/user/me.rs
@@ -6,9 +6,9 @@ use crate::controllers::helpers::*;
use crate::email;
use crate::models::{
- CrateOwner, Email, Follow, NewEmail, OwnerKind, User, Version, VersionOwnerAction,
+ ApiToken, CrateOwner, Email, Follow, NewEmail, OwnerKind, User, Version, VersionOwnerAction,
};
-use crate::schema::{crate_owners, crates, emails, follows, users, versions};
+use crate::schema::{api_tokens, crate_owners, crates, emails, follows, users, versions};
use crate::views::{EncodableMe, EncodableVersion, OwnedCrate};
/// Handles the `GET /me` route.
@@ -27,6 +27,11 @@ pub fn me(req: &mut dyn Request) -> AppResult {
))
.first::<(User, Option, Option, bool)>(&*conn)?;
+ let tokens: Vec = ApiToken::belonging_to(req.user()?)
+ .filter(api_tokens::revoked.eq(false))
+ .load(&*conn)?;
+ let has_tokens = !tokens.is_empty();
+
let owned_crates = CrateOwner::by_owner_kind(OwnerKind::User)
.inner_join(crates::table)
.filter(crate_owners::owner_id.eq(user_id))
@@ -44,7 +49,7 @@ pub fn me(req: &mut dyn Request) -> AppResult {
let verified = verified.unwrap_or(false);
let verification_sent = verified || verification_sent;
Ok(req.json(&EncodableMe {
- user: user.encodable_private(email, verified, verification_sent),
+ user: user.encodable_private(email, verified, verification_sent, has_tokens),
owned_crates,
}))
}
diff --git a/src/models/user.rs b/src/models/user.rs
index d3f1dd3e381..90128e951ac 100644
--- a/src/models/user.rs
+++ b/src/models/user.rs
@@ -171,6 +171,7 @@ impl User {
email: Option,
email_verified: bool,
email_verification_sent: bool,
+ has_tokens: bool,
) -> EncodablePrivateUser {
let User {
id,
@@ -186,6 +187,7 @@ impl User {
email,
email_verified,
email_verification_sent,
+ has_tokens,
avatar: gh_avatar,
login: gh_login,
name,
diff --git a/src/tests/user.rs b/src/tests/user.rs
index ee1e77bc7b4..57a8c61e16a 100644
--- a/src/tests/user.rs
+++ b/src/tests/user.rs
@@ -5,7 +5,7 @@ use crate::{
OkBool, TestApp,
};
use cargo_registry::{
- models::{Email, NewUser, User},
+ models::{ApiToken, Email, NewUser, User},
schema::crate_owners,
views::{EncodablePrivateUser, EncodablePublicUser, EncodableVersion, OwnedCrate},
};
@@ -745,3 +745,19 @@ fn test_update_email_notifications_not_owned() {
// There should be no change to the `email_notifications` value for a crate not belonging to me
assert!(email_notifications);
}
+
+#[test]
+fn shows_that_user_has_tokens() {
+ let (app, _, user) = TestApp::init().with_user();
+
+ let user_id = user.as_model().id;
+ app.db(|conn| {
+ vec![
+ t!(ApiToken::insert(conn, user_id, "bar")),
+ t!(ApiToken::insert(conn, user_id, "baz")),
+ ]
+ });
+
+ let json = user.show_me();
+ assert!(json.user.has_tokens);
+}
diff --git a/src/views.rs b/src/views.rs
index 86e042d350e..38c2feb6998 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -176,6 +176,7 @@ pub struct EncodablePrivateUser {
pub email: Option,
pub avatar: Option,
pub url: Option,
+ pub has_tokens: bool,
}
/// The serialization format for the `User` model.
diff --git a/tests/integration/components/flesh-message-test.js b/tests/integration/components/flesh-message-test.js
new file mode 100644
index 00000000000..4ce750b8033
--- /dev/null
+++ b/tests/integration/components/flesh-message-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | flesh-message', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ assert.expect(2);
+
+ await render(hbs` `);
+
+ assert.dom('[data-test-flash-message]').hasText('test text');
+ assert.dom('[data-test-flash-message]').isVisible();
+ });
+});
diff --git a/tests/integration/components/welcome-message-test.js b/tests/integration/components/welcome-message-test.js
new file mode 100644
index 00000000000..c6077b5f5ed
--- /dev/null
+++ b/tests/integration/components/welcome-message-test.js
@@ -0,0 +1,67 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import setupMirage from '../../helpers/setup-mirage';
+
+module('Integration | Component | welcome-message', function(hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+
+ test('it renders', async function(assert) {
+ assert.expect(2);
+ const user = this.server.create('user');
+
+ this.session = this.owner.lookup('service:session');
+ this.session.loginUser(user);
+
+ await render(hbs`{{welcome-message}}`);
+
+ assert
+ .dom('[data-test-welcome-message]')
+ .hasText('Welcome to crates.io! Visit account settings to verify your email address and create an API token!');
+ assert.dom('[data-test-welcome-message]').isVisible();
+ });
+
+ test('it show reminder about email only if user has tokens', async function(assert) {
+ assert.expect(2);
+ const user = this.server.create('user', 'withTokens');
+
+ this.session = this.owner.lookup('service:session');
+ this.session.loginUser(user);
+
+ await render(hbs`{{welcome-message}}`);
+
+ assert
+ .dom('[data-test-welcome-message]')
+ .hasText('Welcome to crates.io! Visit account settings to verify your email address!');
+ assert.dom('[data-test-welcome-message]').isVisible();
+ });
+
+ test('it show reminder about tokens only if user has verified email', async function(assert) {
+ assert.expect(2);
+ const user = this.server.create('user', 'withVerifiedEmail');
+
+ this.session = this.owner.lookup('service:session');
+ this.session.loginUser(user);
+
+ await render(hbs`{{welcome-message}}`);
+
+ assert
+ .dom('[data-test-welcome-message]')
+ .hasText('Welcome to crates.io! Visit account settings to create an API token!');
+ assert.dom('[data-test-welcome-message]').isVisible();
+ });
+
+ test('it not shows if user has tokens and verified email', async function(assert) {
+ assert.expect(1);
+ const user = this.server.create('user', 'withTokens', 'withVerifiedEmail');
+
+ this.session = this.owner.lookup('service:session');
+ this.session.loginUser(user);
+
+ await render(hbs`{{welcome-message}}`);
+
+ assert.dom('[data-test-welcome-message]').isNotVisible();
+ });
+});
diff --git a/tests/mirage/crates-test.js b/tests/mirage/crates-test.js
index 78f51d5a279..df767e2e906 100644
--- a/tests/mirage/crates-test.js
+++ b/tests/mirage/crates-test.js
@@ -684,25 +684,14 @@ module('Mirage | Crates', function(hooks) {
});
test('returns the list of users that own the specified crate', async function(assert) {
- let user = this.server.create('user', { name: 'John Doe' });
+ let user = this.server.create('user');
this.server.create('crate', { name: 'rand', userOwners: [user] });
let response = await fetch('/api/v1/crates/rand/owner_user');
assert.equal(response.status, 200);
let responsePayload = await response.json();
- assert.deepEqual(responsePayload, {
- users: [
- {
- id: '1',
- avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
- kind: 'user',
- login: 'john-doe',
- name: 'John Doe',
- url: 'https://github.com/john-doe',
- },
- ],
- });
+ assert.deepEqual(JSON.stringify(responsePayload), JSON.stringify({ users: [user] }));
});
});
diff --git a/tests/mirage/users-test.js b/tests/mirage/users-test.js
index 8766a5c98a3..a698ec7ad31 100644
--- a/tests/mirage/users-test.js
+++ b/tests/mirage/users-test.js
@@ -18,21 +18,14 @@ module('Mirage | Users', function(hooks) {
});
test('returns a user object for known users', async function(assert) {
- let user = this.server.create('user', { name: 'John Doe' });
+ let user = this.server.create('user');
let response = await fetch(`/api/v1/users/${user.login}`);
assert.equal(response.status, 200);
let responsePayload = await response.json();
- assert.deepEqual(responsePayload, {
- user: {
- id: '1',
- avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
- login: 'john-doe',
- name: 'John Doe',
- url: 'https://github.com/john-doe',
- },
- });
+
+ assert.deepEqual(JSON.stringify(responsePayload), JSON.stringify({ user }));
});
});
});