Skip to content
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/crates_io_database/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sha2 = "=0.10.8"
thiserror = "=2.0.12"
tracing = "=0.1.41"
unicode-xid = "=0.2.6"
utoipa = "=5.3.1"

[dev-dependencies]
claims = "=0.8.0"
Expand Down
28 changes: 25 additions & 3 deletions crates/crates_io_database/src/models/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,43 @@ impl NewApiToken {
}

/// The model representing a row in the `api_tokens` database table.
#[derive(Debug, Identifiable, Queryable, Selectable, Associations, serde::Serialize)]
#[derive(
Debug, Identifiable, Queryable, Selectable, Associations, serde::Serialize, utoipa::ToSchema,
)]
#[diesel(belongs_to(User))]
pub struct ApiToken {
/// An opaque unique identifier for the token.
#[schema(example = 42)]
pub id: i32,

#[serde(skip)]
pub user_id: i32,

/// The name of the token.
#[schema(example = "Example API Token")]
pub name: String,

/// The date and time when the token was created.
#[schema(example = "2017-01-06T14:23:11Z")]
pub created_at: DateTime<Utc>,

/// The date and time when the token was last used.
#[schema(example = "2021-10-26T11:32:12Z")]
pub last_used_at: Option<DateTime<Utc>>,

#[serde(skip)]
pub revoked: bool,
/// `None` or a list of crate scope patterns (see RFC #2947)

/// `None` or a list of crate scope patterns (see RFC #2947).
#[schema(value_type = Option<Vec<String>>, example = json!(["serde"]))]
pub crate_scopes: Option<Vec<CrateScope>>,
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947)

/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947).
#[schema(example = json!(["publish-update"]))]
pub endpoint_scopes: Option<Vec<EndpointScope>>,

/// The date and time when the token will expire, or `null`.
#[schema(example = "2030-10-26T11:32:12Z")]
pub expired_at: Option<DateTime<Utc>>,
}

Expand Down
4 changes: 3 additions & 1 deletion crates/crates_io_database/src/models/token/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use diesel::serialize::{self, IsNull, Output, ToSql};
use diesel::sql_types::Text;
use std::io::Write;

#[derive(Clone, Copy, Debug, PartialEq, Eq, diesel::AsExpression, serde::Serialize)]
#[derive(
Clone, Copy, Debug, PartialEq, Eq, diesel::AsExpression, serde::Serialize, utoipa::ToSchema,
)]
#[diesel(sql_type = Text)]
#[serde(rename_all = "kebab-case")]
pub enum EndpointScope {
Expand Down
35 changes: 25 additions & 10 deletions src/controllers/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ impl GetParams {
}
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct ListResponse {
pub api_tokens: Vec<ApiToken>,
}

/// List all API tokens of the authenticated user.
#[utoipa::path(
get,
path = "/api/v1/me/tokens",
security(("cookie" = [])),
tag = "api_tokens",
responses((status = 200, description = "Successful Response")),
responses((status = 200, description = "Successful Response", body = inline(ListResponse))),
)]
pub async fn list_api_tokens(
app: AppState,
Expand Down Expand Up @@ -84,19 +89,24 @@ pub struct NewApiTokenRequest {
api_token: NewApiToken,
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct CreateResponse {
api_token: EncodableApiTokenWithToken,
}

/// Create a new API token.
#[utoipa::path(
put,
path = "/api/v1/me/tokens",
security(("cookie" = [])),
tag = "api_tokens",
responses((status = 200, description = "Successful Response")),
responses((status = 200, description = "Successful Response", body = inline(CreateResponse))),
)]
pub async fn create_api_token(
app: AppState,
parts: Parts,
Json(new): Json<NewApiTokenRequest>,
) -> AppResult<ErasedJson> {
) -> AppResult<Json<CreateResponse>> {
if new.api_token.name.is_empty() {
return Err(bad_request("name must have a value"));
}
Expand Down Expand Up @@ -181,7 +191,12 @@ pub async fn create_api_token(
plaintext: plaintext.expose_secret().to_string(),
};

Ok(json!({ "api_token": api_token }))
Ok(Json(CreateResponse { api_token }))
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct GetResponse {
pub api_token: ApiToken,
}

/// Find API token by id.
Expand All @@ -196,23 +211,23 @@ pub async fn create_api_token(
("cookie" = []),
),
tag = "api_tokens",
responses((status = 200, description = "Successful Response")),
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
)]
pub async fn find_api_token(
app: AppState,
Path(id): Path<i32>,
req: Parts,
) -> AppResult<ErasedJson> {
) -> AppResult<Json<GetResponse>> {
let mut conn = app.db_write().await?;
let auth = AuthCheck::default().check(&req, &mut conn).await?;
let user = auth.user();
let token = ApiToken::belonging_to(user)
let api_token = ApiToken::belonging_to(user)
.find(id)
.select(ApiToken::as_select())
.first(&mut conn)
.await?;

Ok(json!({ "api_token": token }))
Ok(Json(GetResponse { api_token }))
}

/// Revoke API token.
Expand All @@ -227,7 +242,7 @@ pub async fn find_api_token(
("cookie" = []),
),
tag = "api_tokens",
responses((status = 200, description = "Successful Response")),
responses((status = 200, description = "Successful Response", body = Object)),
)]
pub async fn revoke_api_token(
app: AppState,
Expand All @@ -254,7 +269,7 @@ pub async fn revoke_api_token(
path = "/api/v1/tokens/current",
security(("api_token" = [])),
tag = "api_tokens",
responses((status = 200, description = "Successful Response")),
responses((status = 204, description = "Successful Response")),
)]
pub async fn revoke_current_api_token(app: AppState, req: Parts) -> AppResult<Response> {
let mut conn = app.db_write().await?;
Expand Down
158 changes: 157 additions & 1 deletion src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,78 @@ expression: response.json()
{
"components": {
"schemas": {
"ApiToken": {
"description": "The model representing a row in the `api_tokens` database table.",
"properties": {
"crate_scopes": {
"description": "`None` or a list of crate scope patterns (see RFC #2947).",
"example": [
"serde"
],
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"created_at": {
"description": "The date and time when the token was created.",
"example": "2017-01-06T14:23:11Z",
"format": "date-time",
"type": "string"
},
"endpoint_scopes": {
"description": "A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947).",
"example": [
"publish-update"
],
"items": {
"$ref": "#/components/schemas/EndpointScope"
},
"type": [
"array",
"null"
]
},
"expired_at": {
"description": "The date and time when the token will expire, or `null`.",
"example": "2030-10-26T11:32:12Z",
"format": "date-time",
"type": [
"string",
"null"
]
},
"id": {
"description": "An opaque unique identifier for the token.",
"example": 42,
"format": "int32",
"type": "integer"
},
"last_used_at": {
"description": "The date and time when the token was last used.",
"example": "2021-10-26T11:32:12Z",
"format": "date-time",
"type": [
"string",
"null"
]
},
"name": {
"description": "The name of the token.",
"example": "Example API Token",
"type": "string"
}
},
"required": [
"id",
"name",
"created_at"
],
"type": "object"
},
"AuthenticatedUser": {
"properties": {
"avatar": {
Expand Down Expand Up @@ -425,6 +497,26 @@ expression: response.json()
],
"type": "object"
},
"EncodableApiTokenWithToken": {
"allOf": [
{
"$ref": "#/components/schemas/ApiToken"
},
{
"properties": {
"token": {
"description": "The plaintext API token.\n\nOnly available when the token is created.",
"example": "a1b2c3d4e5f6g7h8i9j0",
"type": "string"
}
},
"required": [
"token"
],
"type": "object"
}
]
},
"EncodableDependency": {
"properties": {
"crate_id": {
Expand Down Expand Up @@ -497,6 +589,15 @@ expression: response.json()
],
"type": "object"
},
"EndpointScope": {
"enum": [
"publish-new",
"publish-update",
"yank",
"change-owners"
],
"type": "string"
},
"Keyword": {
"properties": {
"crates_cnt": {
Expand Down Expand Up @@ -3551,6 +3652,24 @@ expression: response.json()
"operationId": "list_api_tokens",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"api_tokens": {
"items": {
"$ref": "#/components/schemas/ApiToken"
},
"type": "array"
}
},
"required": [
"api_tokens"
],
"type": "object"
}
}
},
"description": "Successful Response"
}
},
Expand All @@ -3568,6 +3687,21 @@ expression: response.json()
"operationId": "create_api_token",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"api_token": {
"$ref": "#/components/schemas/EncodableApiTokenWithToken"
}
},
"required": [
"api_token"
],
"type": "object"
}
}
},
"description": "Successful Response"
}
},
Expand Down Expand Up @@ -3599,6 +3733,13 @@ expression: response.json()
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": "Successful Response"
}
},
Expand Down Expand Up @@ -3631,6 +3772,21 @@ expression: response.json()
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"api_token": {
"$ref": "#/components/schemas/ApiToken"
}
},
"required": [
"api_token"
],
"type": "object"
}
}
},
"description": "Successful Response"
}
},
Expand Down Expand Up @@ -3876,7 +4032,7 @@ expression: response.json()
"description": "This endpoint revokes the API token that is used to authenticate\nthe request.",
"operationId": "revoke_current_api_token",
"responses": {
"200": {
"204": {
"description": "Successful Response"
}
},
Expand Down
Loading