Skip to content

Commit 037f953

Browse files
authored
Merge pull request #10752 from Turbo87/openapi
Improve OpenAPI documentation for API tokens endpoints
2 parents 58f9d76 + bfa1702 commit 037f953

File tree

7 files changed

+218
-19
lines changed

7 files changed

+218
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/crates_io_database/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sha2 = "=0.10.8"
2525
thiserror = "=2.0.12"
2626
tracing = "=0.1.41"
2727
unicode-xid = "=0.2.6"
28+
utoipa = "=5.3.1"
2829

2930
[dev-dependencies]
3031
claims = "=0.8.0"

crates/crates_io_database/src/models/token.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,43 @@ impl NewApiToken {
3939
}
4040

4141
/// The model representing a row in the `api_tokens` database table.
42-
#[derive(Debug, Identifiable, Queryable, Selectable, Associations, serde::Serialize)]
42+
#[derive(
43+
Debug, Identifiable, Queryable, Selectable, Associations, serde::Serialize, utoipa::ToSchema,
44+
)]
4345
#[diesel(belongs_to(User))]
4446
pub struct ApiToken {
47+
/// An opaque unique identifier for the token.
48+
#[schema(example = 42)]
4549
pub id: i32,
50+
4651
#[serde(skip)]
4752
pub user_id: i32,
53+
54+
/// The name of the token.
55+
#[schema(example = "Example API Token")]
4856
pub name: String,
57+
58+
/// The date and time when the token was created.
59+
#[schema(example = "2017-01-06T14:23:11Z")]
4960
pub created_at: DateTime<Utc>,
61+
62+
/// The date and time when the token was last used.
63+
#[schema(example = "2021-10-26T11:32:12Z")]
5064
pub last_used_at: Option<DateTime<Utc>>,
65+
5166
#[serde(skip)]
5267
pub revoked: bool,
53-
/// `None` or a list of crate scope patterns (see RFC #2947)
68+
69+
/// `None` or a list of crate scope patterns (see RFC #2947).
70+
#[schema(value_type = Option<Vec<String>>, example = json!(["serde"]))]
5471
pub crate_scopes: Option<Vec<CrateScope>>,
55-
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947)
72+
73+
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947).
74+
#[schema(example = json!(["publish-update"]))]
5675
pub endpoint_scopes: Option<Vec<EndpointScope>>,
76+
77+
/// The date and time when the token will expire, or `null`.
78+
#[schema(example = "2030-10-26T11:32:12Z")]
5779
pub expired_at: Option<DateTime<Utc>>,
5880
}
5981

crates/crates_io_database/src/models/token/scopes.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use diesel::serialize::{self, IsNull, Output, ToSql};
55
use diesel::sql_types::Text;
66
use std::io::Write;
77

8-
#[derive(Clone, Copy, Debug, PartialEq, Eq, diesel::AsExpression, serde::Serialize)]
8+
#[derive(
9+
Clone, Copy, Debug, PartialEq, Eq, diesel::AsExpression, serde::Serialize, utoipa::ToSchema,
10+
)]
911
#[diesel(sql_type = Text)]
1012
#[serde(rename_all = "kebab-case")]
1113
pub enum EndpointScope {

src/controllers/token.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,18 @@ impl GetParams {
3737
}
3838
}
3939

40+
#[derive(Debug, Serialize, utoipa::ToSchema)]
41+
pub struct ListResponse {
42+
pub api_tokens: Vec<ApiToken>,
43+
}
44+
4045
/// List all API tokens of the authenticated user.
4146
#[utoipa::path(
4247
get,
4348
path = "/api/v1/me/tokens",
4449
security(("cookie" = [])),
4550
tag = "api_tokens",
46-
responses((status = 200, description = "Successful Response")),
51+
responses((status = 200, description = "Successful Response", body = inline(ListResponse))),
4752
)]
4853
pub async fn list_api_tokens(
4954
app: AppState,
@@ -84,19 +89,24 @@ pub struct NewApiTokenRequest {
8489
api_token: NewApiToken,
8590
}
8691

92+
#[derive(Debug, Serialize, utoipa::ToSchema)]
93+
pub struct CreateResponse {
94+
api_token: EncodableApiTokenWithToken,
95+
}
96+
8797
/// Create a new API token.
8898
#[utoipa::path(
8999
put,
90100
path = "/api/v1/me/tokens",
91101
security(("cookie" = [])),
92102
tag = "api_tokens",
93-
responses((status = 200, description = "Successful Response")),
103+
responses((status = 200, description = "Successful Response", body = inline(CreateResponse))),
94104
)]
95105
pub async fn create_api_token(
96106
app: AppState,
97107
parts: Parts,
98108
Json(new): Json<NewApiTokenRequest>,
99-
) -> AppResult<ErasedJson> {
109+
) -> AppResult<Json<CreateResponse>> {
100110
if new.api_token.name.is_empty() {
101111
return Err(bad_request("name must have a value"));
102112
}
@@ -181,7 +191,12 @@ pub async fn create_api_token(
181191
plaintext: plaintext.expose_secret().to_string(),
182192
};
183193

184-
Ok(json!({ "api_token": api_token }))
194+
Ok(Json(CreateResponse { api_token }))
195+
}
196+
197+
#[derive(Debug, Serialize, utoipa::ToSchema)]
198+
pub struct GetResponse {
199+
pub api_token: ApiToken,
185200
}
186201

187202
/// Find API token by id.
@@ -196,23 +211,23 @@ pub async fn create_api_token(
196211
("cookie" = []),
197212
),
198213
tag = "api_tokens",
199-
responses((status = 200, description = "Successful Response")),
214+
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
200215
)]
201216
pub async fn find_api_token(
202217
app: AppState,
203218
Path(id): Path<i32>,
204219
req: Parts,
205-
) -> AppResult<ErasedJson> {
220+
) -> AppResult<Json<GetResponse>> {
206221
let mut conn = app.db_write().await?;
207222
let auth = AuthCheck::default().check(&req, &mut conn).await?;
208223
let user = auth.user();
209-
let token = ApiToken::belonging_to(user)
224+
let api_token = ApiToken::belonging_to(user)
210225
.find(id)
211226
.select(ApiToken::as_select())
212227
.first(&mut conn)
213228
.await?;
214229

215-
Ok(json!({ "api_token": token }))
230+
Ok(Json(GetResponse { api_token }))
216231
}
217232

218233
/// Revoke API token.
@@ -227,7 +242,7 @@ pub async fn find_api_token(
227242
("cookie" = []),
228243
),
229244
tag = "api_tokens",
230-
responses((status = 200, description = "Successful Response")),
245+
responses((status = 200, description = "Successful Response", body = Object)),
231246
)]
232247
pub async fn revoke_api_token(
233248
app: AppState,
@@ -254,7 +269,7 @@ pub async fn revoke_api_token(
254269
path = "/api/v1/tokens/current",
255270
security(("api_token" = [])),
256271
tag = "api_tokens",
257-
responses((status = 200, description = "Successful Response")),
272+
responses((status = 204, description = "Successful Response")),
258273
)]
259274
pub async fn revoke_current_api_token(app: AppState, req: Parts) -> AppResult<Response> {
260275
let mut conn = app.db_write().await?;

src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,78 @@ expression: response.json()
55
{
66
"components": {
77
"schemas": {
8+
"ApiToken": {
9+
"description": "The model representing a row in the `api_tokens` database table.",
10+
"properties": {
11+
"crate_scopes": {
12+
"description": "`None` or a list of crate scope patterns (see RFC #2947).",
13+
"example": [
14+
"serde"
15+
],
16+
"items": {
17+
"type": "string"
18+
},
19+
"type": [
20+
"array",
21+
"null"
22+
]
23+
},
24+
"created_at": {
25+
"description": "The date and time when the token was created.",
26+
"example": "2017-01-06T14:23:11Z",
27+
"format": "date-time",
28+
"type": "string"
29+
},
30+
"endpoint_scopes": {
31+
"description": "A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947).",
32+
"example": [
33+
"publish-update"
34+
],
35+
"items": {
36+
"$ref": "#/components/schemas/EndpointScope"
37+
},
38+
"type": [
39+
"array",
40+
"null"
41+
]
42+
},
43+
"expired_at": {
44+
"description": "The date and time when the token will expire, or `null`.",
45+
"example": "2030-10-26T11:32:12Z",
46+
"format": "date-time",
47+
"type": [
48+
"string",
49+
"null"
50+
]
51+
},
52+
"id": {
53+
"description": "An opaque unique identifier for the token.",
54+
"example": 42,
55+
"format": "int32",
56+
"type": "integer"
57+
},
58+
"last_used_at": {
59+
"description": "The date and time when the token was last used.",
60+
"example": "2021-10-26T11:32:12Z",
61+
"format": "date-time",
62+
"type": [
63+
"string",
64+
"null"
65+
]
66+
},
67+
"name": {
68+
"description": "The name of the token.",
69+
"example": "Example API Token",
70+
"type": "string"
71+
}
72+
},
73+
"required": [
74+
"id",
75+
"name",
76+
"created_at"
77+
],
78+
"type": "object"
79+
},
880
"AuthenticatedUser": {
981
"properties": {
1082
"avatar": {
@@ -425,6 +497,26 @@ expression: response.json()
425497
],
426498
"type": "object"
427499
},
500+
"EncodableApiTokenWithToken": {
501+
"allOf": [
502+
{
503+
"$ref": "#/components/schemas/ApiToken"
504+
},
505+
{
506+
"properties": {
507+
"token": {
508+
"description": "The plaintext API token.\n\nOnly available when the token is created.",
509+
"example": "a1b2c3d4e5f6g7h8i9j0",
510+
"type": "string"
511+
}
512+
},
513+
"required": [
514+
"token"
515+
],
516+
"type": "object"
517+
}
518+
]
519+
},
428520
"EncodableDependency": {
429521
"properties": {
430522
"crate_id": {
@@ -497,6 +589,15 @@ expression: response.json()
497589
],
498590
"type": "object"
499591
},
592+
"EndpointScope": {
593+
"enum": [
594+
"publish-new",
595+
"publish-update",
596+
"yank",
597+
"change-owners"
598+
],
599+
"type": "string"
600+
},
500601
"Keyword": {
501602
"properties": {
502603
"crates_cnt": {
@@ -3551,6 +3652,24 @@ expression: response.json()
35513652
"operationId": "list_api_tokens",
35523653
"responses": {
35533654
"200": {
3655+
"content": {
3656+
"application/json": {
3657+
"schema": {
3658+
"properties": {
3659+
"api_tokens": {
3660+
"items": {
3661+
"$ref": "#/components/schemas/ApiToken"
3662+
},
3663+
"type": "array"
3664+
}
3665+
},
3666+
"required": [
3667+
"api_tokens"
3668+
],
3669+
"type": "object"
3670+
}
3671+
}
3672+
},
35543673
"description": "Successful Response"
35553674
}
35563675
},
@@ -3568,6 +3687,21 @@ expression: response.json()
35683687
"operationId": "create_api_token",
35693688
"responses": {
35703689
"200": {
3690+
"content": {
3691+
"application/json": {
3692+
"schema": {
3693+
"properties": {
3694+
"api_token": {
3695+
"$ref": "#/components/schemas/EncodableApiTokenWithToken"
3696+
}
3697+
},
3698+
"required": [
3699+
"api_token"
3700+
],
3701+
"type": "object"
3702+
}
3703+
}
3704+
},
35713705
"description": "Successful Response"
35723706
}
35733707
},
@@ -3599,6 +3733,13 @@ expression: response.json()
35993733
],
36003734
"responses": {
36013735
"200": {
3736+
"content": {
3737+
"application/json": {
3738+
"schema": {
3739+
"type": "object"
3740+
}
3741+
}
3742+
},
36023743
"description": "Successful Response"
36033744
}
36043745
},
@@ -3631,6 +3772,21 @@ expression: response.json()
36313772
],
36323773
"responses": {
36333774
"200": {
3775+
"content": {
3776+
"application/json": {
3777+
"schema": {
3778+
"properties": {
3779+
"api_token": {
3780+
"$ref": "#/components/schemas/ApiToken"
3781+
}
3782+
},
3783+
"required": [
3784+
"api_token"
3785+
],
3786+
"type": "object"
3787+
}
3788+
}
3789+
},
36343790
"description": "Successful Response"
36353791
}
36363792
},
@@ -3876,7 +4032,7 @@ expression: response.json()
38764032
"description": "This endpoint revokes the API token that is used to authenticate\nthe request.",
38774033
"operationId": "revoke_current_api_token",
38784034
"responses": {
3879-
"200": {
4035+
"204": {
38804036
"description": "Successful Response"
38814037
}
38824038
},

0 commit comments

Comments
 (0)