Skip to content

Commit fbec41b

Browse files
authored
Merge pull request #10557 from Turbo87/new-token
models/token: Extract `NewApiToken` struct
2 parents 0b736b4 + 2e0bc79 commit fbec41b

File tree

8 files changed

+136
-163
lines changed

8 files changed

+136
-163
lines changed

src/controllers/token.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::app::AppState;
77
use crate::auth::AuthCheck;
88
use crate::models::token::{CrateScope, EndpointScope};
99
use crate::util::errors::{bad_request, AppResult};
10+
use crate::util::token::PlainToken;
1011
use axum::extract::{Path, Query};
1112
use axum::response::{IntoResponse, Response};
1213
use axum::Json;
@@ -19,6 +20,7 @@ use diesel::prelude::*;
1920
use diesel_async::RunQueryDsl;
2021
use http::request::Parts;
2122
use http::StatusCode;
23+
use secrecy::ExposeSecret;
2224

2325
#[derive(Deserialize)]
2426
pub struct GetParams {
@@ -148,15 +150,16 @@ pub async fn create_api_token(
148150

149151
let recipient = user.email(&mut conn).await?;
150152

151-
let api_token = ApiToken::insert_with_scopes(
152-
&mut conn,
153-
user.id,
154-
&new.api_token.name,
155-
crate_scopes,
156-
endpoint_scopes,
157-
new.api_token.expired_at,
158-
)
159-
.await?;
153+
let plaintext = PlainToken::generate();
154+
155+
let new_token = crate::models::token::NewApiToken::builder()
156+
.user_id(user.id)
157+
.name(&new.api_token.name)
158+
.token(plaintext.hashed())
159+
.maybe_crate_scopes(crate_scopes)
160+
.maybe_endpoint_scopes(endpoint_scopes)
161+
.maybe_expired_at(new.api_token.expired_at)
162+
.build();
160163

161164
if let Some(recipient) = recipient {
162165
let email = NewTokenEmail {
@@ -174,7 +177,10 @@ pub async fn create_api_token(
174177
}
175178
}
176179

177-
let api_token = EncodableApiTokenWithToken::from(api_token);
180+
let api_token = EncodableApiTokenWithToken {
181+
token: new_token.insert(&mut conn).await?,
182+
plaintext: plaintext.expose_secret().to_string(),
183+
};
178184

179185
Ok(json!({ "api_token": api_token }))
180186
}

src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub use self::krate::{Crate, CrateName, NewCrate, RecentCrateDownloads};
1414
pub use self::owner::{CrateOwner, Owner, OwnerKind};
1515
pub use self::rights::Rights;
1616
pub use self::team::{NewTeam, Team};
17-
pub use self::token::{ApiToken, CreatedApiToken};
17+
pub use self::token::ApiToken;
1818
pub use self::user::{NewUser, User};
1919
pub use self::version::{NewVersion, TopVersions, Version};
2020

src/models/token.rs

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod scopes;
22

3+
use bon::Builder;
34
use chrono::NaiveDateTime;
45
use diesel::dsl::now;
56
use diesel::prelude::*;
@@ -12,6 +13,31 @@ use crate::schema::api_tokens;
1213
use crate::util::rfc3339;
1314
use crate::util::token::{HashedToken, PlainToken};
1415

16+
#[derive(Debug, Insertable, Builder)]
17+
#[diesel(table_name = api_tokens, check_for_backend(diesel::pg::Pg))]
18+
pub struct NewApiToken {
19+
pub user_id: i32,
20+
#[builder(into)]
21+
pub name: String,
22+
#[builder(default = PlainToken::generate().hashed())]
23+
pub token: HashedToken,
24+
/// `None` or a list of crate scope patterns (see RFC #2947)
25+
pub crate_scopes: Option<Vec<CrateScope>>,
26+
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947)
27+
pub endpoint_scopes: Option<Vec<EndpointScope>>,
28+
pub expired_at: Option<NaiveDateTime>,
29+
}
30+
31+
impl NewApiToken {
32+
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<ApiToken> {
33+
diesel::insert_into(api_tokens::table)
34+
.values(self)
35+
.returning(ApiToken::as_returning())
36+
.get_result(conn)
37+
.await
38+
}
39+
}
40+
1541
/// The model representing a row in the `api_tokens` database table.
1642
#[derive(Debug, Identifiable, Queryable, Selectable, Associations, Serialize)]
1743
#[diesel(belongs_to(User))]
@@ -35,44 +61,6 @@ pub struct ApiToken {
3561
}
3662

3763
impl ApiToken {
38-
/// Generates a new named API token for a user
39-
pub async fn insert(
40-
conn: &mut AsyncPgConnection,
41-
user_id: i32,
42-
name: &str,
43-
) -> QueryResult<CreatedApiToken> {
44-
Self::insert_with_scopes(conn, user_id, name, None, None, None).await
45-
}
46-
47-
pub async fn insert_with_scopes(
48-
conn: &mut AsyncPgConnection,
49-
user_id: i32,
50-
name: &str,
51-
crate_scopes: Option<Vec<CrateScope>>,
52-
endpoint_scopes: Option<Vec<EndpointScope>>,
53-
expired_at: Option<NaiveDateTime>,
54-
) -> QueryResult<CreatedApiToken> {
55-
let token = PlainToken::generate();
56-
57-
let model: ApiToken = diesel::insert_into(api_tokens::table)
58-
.values((
59-
api_tokens::user_id.eq(user_id),
60-
api_tokens::name.eq(name),
61-
api_tokens::token.eq(token.hashed()),
62-
api_tokens::crate_scopes.eq(crate_scopes),
63-
api_tokens::endpoint_scopes.eq(endpoint_scopes),
64-
api_tokens::expired_at.eq(expired_at),
65-
))
66-
.returning(ApiToken::as_returning())
67-
.get_result(conn)
68-
.await?;
69-
70-
Ok(CreatedApiToken {
71-
plaintext: token,
72-
model,
73-
})
74-
}
75-
7664
pub async fn find_by_api_token(
7765
conn: &mut AsyncPgConnection,
7866
token: &HashedToken,
@@ -107,12 +95,6 @@ impl ApiToken {
10795
}
10896
}
10997

110-
#[derive(Debug)]
111-
pub struct CreatedApiToken {
112-
pub model: ApiToken,
113-
pub plaintext: PlainToken,
114-
}
115-
11698
#[cfg(test)]
11799
mod tests {
118100
use super::*;

src/tests/routes/me/tokens/create.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::models::token::{CrateScope, EndpointScope};
1+
use crate::models::token::{CrateScope, EndpointScope, NewApiToken};
22
use crate::models::ApiToken;
33
use crate::tests::util::insta::{self, assert_json_snapshot};
44
use crate::tests::util::{RequestHelper, TestApp};
@@ -46,7 +46,9 @@ async fn create_token_exceeded_tokens_per_user() {
4646
let id = user.as_model().id;
4747

4848
for i in 0..1000 {
49-
assert_ok!(ApiToken::insert(&mut conn, id, &format!("token {i}")).await);
49+
let name = format!("token {i}");
50+
let new_token = NewApiToken::builder().name(name).user_id(id).build();
51+
assert_ok!(new_token.insert(&mut conn).await);
5052
}
5153

5254
let response = user.put::<()>("/api/v1/me/tokens", NEW_BAR).await;

src/tests/routes/me/tokens/get.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::models::token::{CrateScope, EndpointScope};
2-
use crate::models::ApiToken;
1+
use crate::models::token::{CrateScope, EndpointScope, NewApiToken};
32
use crate::tests::util::{RequestHelper, TestApp};
43
use chrono::{Duration, Utc};
54
use http::StatusCode;
@@ -31,23 +30,22 @@ async fn show_token_with_scopes() {
3130
let user_model = user.as_model();
3231
let id = user_model.id;
3332

34-
assert_ok!(ApiToken::insert(&mut conn, id, "bar").await);
35-
let token = assert_ok!(
36-
ApiToken::insert_with_scopes(
37-
&mut conn,
38-
id,
39-
"baz",
40-
Some(vec![
41-
CrateScope::try_from("serde").unwrap(),
42-
CrateScope::try_from("serde-*").unwrap()
43-
]),
44-
Some(vec![EndpointScope::PublishUpdate]),
45-
Some((Utc::now() - Duration::days(31)).naive_utc()),
46-
)
47-
.await
48-
);
33+
let new_token = NewApiToken::builder().name("bar").user_id(id).build();
34+
assert_ok!(new_token.insert(&mut conn).await);
4935

50-
let url = format!("/api/v1/me/tokens/{}", token.model.id);
36+
let new_token = NewApiToken::builder()
37+
.name("baz")
38+
.user_id(id)
39+
.crate_scopes(vec![
40+
CrateScope::try_from("serde").unwrap(),
41+
CrateScope::try_from("serde-*").unwrap(),
42+
])
43+
.endpoint_scopes(vec![EndpointScope::PublishUpdate])
44+
.expired_at((Utc::now() - Duration::days(31)).naive_utc())
45+
.build();
46+
let token = assert_ok!(new_token.insert(&mut conn).await);
47+
48+
let url = format!("/api/v1/me/tokens/{}", token.id);
5149
let response = user.get::<()>(&url).await;
5250
assert_eq!(response.status(), StatusCode::OK);
5351
assert_json_snapshot!(response.json(), {
@@ -70,9 +68,10 @@ async fn show_other_user_token() {
7068
let user2 = app.db_new_user("baz").await;
7169
let user2 = user2.as_model();
7270

73-
let token = assert_ok!(ApiToken::insert(&mut conn, user2.id, "bar").await);
71+
let new_token = NewApiToken::builder().name("bar").user_id(user2.id).build();
72+
let token = assert_ok!(new_token.insert(&mut conn).await);
7473

75-
let url = format!("/api/v1/me/tokens/{}", token.model.id);
74+
let url = format!("/api/v1/me/tokens/{}", token.id);
7675
let response = user1.get::<()>(&url).await;
7776
assert_eq!(response.status(), StatusCode::NOT_FOUND);
7877
}

src/tests/routes/me/tokens/list.rs

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::models::token::{CrateScope, EndpointScope};
2-
use crate::models::ApiToken;
1+
use crate::models::token::{CrateScope, EndpointScope, NewApiToken};
32
use crate::tests::util::insta::{self, assert_json_snapshot};
43
use crate::tests::util::{RequestHelper, TestApp};
54
use chrono::{Duration, Utc};
@@ -33,32 +32,26 @@ async fn list_tokens() {
3332
let mut conn = app.db_conn().await;
3433
let id = user.as_model().id;
3534

36-
assert_ok!(ApiToken::insert(&mut conn, id, "bar").await);
37-
assert_ok!(
38-
ApiToken::insert_with_scopes(
39-
&mut conn,
40-
id,
41-
"baz",
42-
Some(vec![
43-
CrateScope::try_from("serde").unwrap(),
44-
CrateScope::try_from("serde-*").unwrap()
45-
]),
46-
Some(vec![EndpointScope::PublishUpdate]),
47-
None
48-
)
49-
.await
50-
);
51-
assert_ok!(
52-
ApiToken::insert_with_scopes(
53-
&mut conn,
54-
id,
55-
"qux",
56-
None,
57-
None,
58-
Some((Utc::now() - Duration::days(1)).naive_utc()),
59-
)
60-
.await
61-
);
35+
let new_token = NewApiToken::builder().name("bar").user_id(id).build();
36+
assert_ok!(new_token.insert(&mut conn).await);
37+
38+
let new_token = NewApiToken::builder()
39+
.name("baz")
40+
.user_id(id)
41+
.crate_scopes(vec![
42+
CrateScope::try_from("serde").unwrap(),
43+
CrateScope::try_from("serde-*").unwrap(),
44+
])
45+
.endpoint_scopes(vec![EndpointScope::PublishUpdate])
46+
.build();
47+
assert_ok!(new_token.insert(&mut conn).await);
48+
49+
let new_token = NewApiToken::builder()
50+
.name("qux")
51+
.user_id(id)
52+
.expired_at((Utc::now() - Duration::days(1)).naive_utc())
53+
.build();
54+
assert_ok!(new_token.insert(&mut conn).await);
6255

6356
let response = user.get::<()>("/api/v1/me/tokens").await;
6457
assert_eq!(response.status(), StatusCode::OK);
@@ -80,32 +73,27 @@ async fn list_recently_expired_tokens() {
8073
let mut conn = app.db_conn().await;
8174
let id = user.as_model().id;
8275

83-
assert_ok!(ApiToken::insert(&mut conn, id, "bar").await);
84-
assert_ok!(
85-
ApiToken::insert_with_scopes(
86-
&mut conn,
87-
id,
88-
"ancient",
89-
Some(vec![
90-
CrateScope::try_from("serde").unwrap(),
91-
CrateScope::try_from("serde-*").unwrap()
92-
]),
93-
Some(vec![EndpointScope::PublishUpdate]),
94-
Some((Utc::now() - Duration::days(31)).naive_utc()),
95-
)
96-
.await
97-
);
98-
assert_ok!(
99-
ApiToken::insert_with_scopes(
100-
&mut conn,
101-
id,
102-
"recent",
103-
None,
104-
None,
105-
Some((Utc::now() - Duration::days(1)).naive_utc()),
106-
)
107-
.await
108-
);
76+
let new_token = NewApiToken::builder().name("bar").user_id(id).build();
77+
assert_ok!(new_token.insert(&mut conn).await);
78+
79+
let new_token = NewApiToken::builder()
80+
.name("ancient")
81+
.user_id(id)
82+
.crate_scopes(vec![
83+
CrateScope::try_from("serde").unwrap(),
84+
CrateScope::try_from("serde-*").unwrap(),
85+
])
86+
.endpoint_scopes(vec![EndpointScope::PublishUpdate])
87+
.expired_at((Utc::now() - Duration::days(31)).naive_utc())
88+
.build();
89+
assert_ok!(new_token.insert(&mut conn).await);
90+
91+
let new_token = NewApiToken::builder()
92+
.name("recent")
93+
.user_id(id)
94+
.expired_at((Utc::now() - Duration::days(1)).naive_utc())
95+
.build();
96+
assert_ok!(new_token.insert(&mut conn).await);
10997

11098
let response = user.get::<()>("/api/v1/me/tokens?expired_days=30").await;
11199
assert_eq!(response.status(), StatusCode::OK);
@@ -131,8 +119,11 @@ async fn list_tokens_exclude_revoked() {
131119
let mut conn = app.db_conn().await;
132120
let id = user.as_model().id;
133121

134-
let token1 = assert_ok!(ApiToken::insert(&mut conn, id, "bar").await);
135-
assert_ok!(ApiToken::insert(&mut conn, id, "baz").await);
122+
let new_token = NewApiToken::builder().name("bar").user_id(id).build();
123+
let token1 = assert_ok!(new_token.insert(&mut conn).await);
124+
125+
let new_token = NewApiToken::builder().name("baz").user_id(id).build();
126+
assert_ok!(new_token.insert(&mut conn).await);
136127

137128
// List tokens expecting them all to be there.
138129
let response = user.get::<()>("/api/v1/me/tokens").await;
@@ -143,7 +134,7 @@ async fn list_tokens_exclude_revoked() {
143134

144135
// Revoke the first token.
145136
let response = user
146-
.delete::<()>(&format!("/api/v1/me/tokens/{}", token1.model.id))
137+
.delete::<()>(&format!("/api/v1/me/tokens/{}", token1.id))
147138
.await;
148139
assert_eq!(response.status(), StatusCode::OK);
149140

0 commit comments

Comments
 (0)