mirror of
https://github.com/meilisearch/meilisearch.git
synced 2024-11-22 10:07:40 +08:00
implements more route
This commit is contained in:
parent
143a866b48
commit
e9d74d424b
@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::{format_description, time};
|
||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError};
|
||||
@ -32,19 +33,31 @@ impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for Dese
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct CreateApiKey {
|
||||
/// A description for the key. `null` if empty.
|
||||
#[schema(example = json!(null))]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
|
||||
pub description: Option<String>,
|
||||
/// A human-readable name for the key. `null` if empty.
|
||||
#[schema(example = "Indexing Products API key")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
|
||||
pub name: Option<String>,
|
||||
/// A uuid v4 to identify the API Key. If not specified, it's generated by Meilisearch.
|
||||
#[schema(value_type = Uuid, example = json!(null))]
|
||||
#[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)]
|
||||
pub uid: KeyId,
|
||||
/// A list of actions permitted for the key. `["*"]` for all actions. The `*` character can be used as a wildcard when located at the last position. e.g. `documents.*` to authorize access on all documents endpoints.
|
||||
#[schema(example = json!(["documents.add"]))]
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
|
||||
pub actions: Vec<Action>,
|
||||
/// A list of accesible indexes permitted for the key. `["*"]` for all indexes. The `*` character can be used as a wildcard when located at the last position. e.g. `products_*` to allow access to all indexes whose names start with `products_`.
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
|
||||
#[schema(value_type = Vec<String>, example = json!(["products"]))]
|
||||
pub indexes: Vec<IndexUidPattern>,
|
||||
/// Represent the expiration date and time as RFC 3339 format. `null` equals to no expiration time.
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
|
||||
pub expires_at: Option<OffsetDateTime>,
|
||||
}
|
||||
@ -179,7 +192,9 @@ fn parse_expiration_date(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)]
|
||||
#[derive(
|
||||
Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr, ToSchema,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Action {
|
||||
#[serde(rename = "*")]
|
||||
|
@ -150,7 +150,7 @@ pub enum KindWithContent {
|
||||
SnapshotCreation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexSwap {
|
||||
pub indexes: (String, String),
|
||||
|
@ -13,6 +13,7 @@ use meilisearch_types::error::{Code, ResponseError};
|
||||
use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::{IntoParams, OpenApi, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::PAGINATION_DEFAULT_LIMIT;
|
||||
@ -21,6 +22,20 @@ use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::Pagination;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(create_api_key, list_api_keys),
|
||||
tags((
|
||||
name = "Keys",
|
||||
description = "Manage API `keys` for a Meilisearch instance. Each key has a given set of permissions.
|
||||
You must have the master key or the default admin key to access the keys route. More information about the keys and their rights.
|
||||
Accessing any route under `/keys` without having set a master key will result in an error.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/keys"),
|
||||
|
||||
)),
|
||||
)]
|
||||
pub struct ApiKeyApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -35,6 +50,52 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// Create an API Key
|
||||
///
|
||||
/// Create an API Key.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.create", "keys.*", "*"])),
|
||||
request_body = CreateApiKey,
|
||||
responses(
|
||||
(status = 202, description = "Key has been created", body = KeyView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "Indexing Products API key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"products"
|
||||
],
|
||||
"expiresAt": "2021-11-13T00:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn create_api_key(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
|
||||
body: AwebJson<CreateApiKey, DeserrJsonError>,
|
||||
@ -51,11 +112,14 @@ pub async fn create_api_key(
|
||||
Ok(HttpResponse::Created().json(res))
|
||||
}
|
||||
|
||||
#[derive(Deserr, Debug, Clone, Copy)]
|
||||
#[derive(Deserr, Debug, Clone, Copy, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
pub struct ListApiKeys {
|
||||
#[into_params(value_type = usize, default = 0)]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
|
||||
pub offset: Param<usize>,
|
||||
#[into_params(value_type = usize, default = PAGINATION_DEFAULT_LIMIT)]
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
|
||||
pub limit: Param<usize>,
|
||||
}
|
||||
@ -66,6 +130,59 @@ impl ListApiKeys {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get API Keys
|
||||
///
|
||||
/// List all API Keys
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.get", "keys.*", "*"])),
|
||||
params(ListApiKeys),
|
||||
responses(
|
||||
(status = 202, description = "List of keys", body = PaginationView<KeyView>, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "An API Key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"movies"
|
||||
],
|
||||
"expiresAt": "2022-11-12T10:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"total": 1
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn list_api_keys(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
|
||||
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
|
||||
@ -144,19 +261,28 @@ pub struct AuthParam {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyView {
|
||||
pub(super) struct KeyView {
|
||||
/// The name of the API Key if any
|
||||
name: Option<String>,
|
||||
/// The description of the API Key if any
|
||||
description: Option<String>,
|
||||
/// The actual API Key you can send to Meilisearch
|
||||
key: String,
|
||||
/// The `Uuid` specified while creating the key or autogenerated by Meilisearch.
|
||||
uid: Uuid,
|
||||
/// The actions accessible with this key.
|
||||
actions: Vec<Action>,
|
||||
/// The indexes accessible with this key.
|
||||
indexes: Vec<String>,
|
||||
/// The expiration date of the key. Once this timestamp is exceeded the key is not deleted but cannot be used anymore.
|
||||
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
|
||||
expires_at: Option<OffsetDateTime>,
|
||||
/// The date of creation of this API Key.
|
||||
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
|
||||
created_at: OffsetDateTime,
|
||||
/// The date of the last update made on this key.
|
||||
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
|
||||
updated_at: OffsetDateTime,
|
||||
}
|
||||
|
@ -297,7 +297,8 @@ fn entry_stream(
|
||||
2024-10-08T13:35:02.643750Z WARN HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: tracing_actix_web::middleware: Error encountered while processing the incoming HTTP request: ResponseError { code: 400, message: "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625", error_code: "feature_not_enabled", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#feature_not_enabled" }
|
||||
2024-10-08T13:35:02.644191Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: meilisearch: close time.busy=1.66ms time.idle=658µs
|
||||
2024-10-08T13:35:18.564152Z INFO HTTP request{method=PATCH host="localhost:7700" route=/experimental-features query_parameters= user_agent=curl/8.6.0 status_code=200}: meilisearch: close time.busy=1.17ms time.idle=127µs
|
||||
2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs "#
|
||||
2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs
|
||||
"#
|
||||
)),
|
||||
(status = 400, description = "The route is already being used", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ use index_scheduler::IndexScheduler;
|
||||
use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::ErrorType;
|
||||
use meilisearch_types::error::{Code, ResponseError};
|
||||
use meilisearch_types::keys::CreateApiKey;
|
||||
use meilisearch_types::settings::Checked;
|
||||
use meilisearch_types::settings::FacetingSettings;
|
||||
use meilisearch_types::settings::MinWordSizeTyposSetting;
|
||||
@ -27,6 +28,8 @@ use utoipa::ToSchema;
|
||||
use utoipa_rapidoc::RapiDoc;
|
||||
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
||||
|
||||
use self::api_key::KeyView;
|
||||
use self::api_key::ListApiKeys;
|
||||
use self::indexes::IndexStats;
|
||||
use self::logs::GetLogs;
|
||||
use self::logs::LogMode;
|
||||
@ -54,32 +57,33 @@ pub mod tasks;
|
||||
(path = "/tasks", api = tasks::TaskApi),
|
||||
(path = "/snapshots", api = snapshot::SnapshotApi),
|
||||
(path = "/dumps", api = dump::DumpApi),
|
||||
(path = "/keys", api = api_key::ApiKeyApi),
|
||||
(path = "/metrics", api = metrics::MetricApi),
|
||||
(path = "/logs", api = logs::LogsApi),
|
||||
),
|
||||
paths(get_health, get_version, get_stats),
|
||||
modifiers(&OpenApiAuth),
|
||||
components(schemas(UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind))
|
||||
components(schemas(PaginationView<KeyView>, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind))
|
||||
)]
|
||||
pub struct MeilisearchApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
let openapi = MeilisearchApi::openapi();
|
||||
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure))
|
||||
.service(Scalar::with_url("/scalar", openapi.clone()))
|
||||
.service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi).path("/rapidoc"))
|
||||
.service(web::resource("/health").route(web::get().to(get_health)))
|
||||
.service(web::scope("/logs").configure(logs::configure))
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure)) // done
|
||||
.service(Scalar::with_url("/scalar", openapi.clone())) // done
|
||||
.service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi).path("/rapidoc")) // done
|
||||
.service(web::resource("/health").route(web::get().to(get_health))) // done
|
||||
.service(web::scope("/logs").configure(logs::configure)) // done
|
||||
.service(web::scope("/keys").configure(api_key::configure))
|
||||
.service(web::scope("/dumps").configure(dump::configure))
|
||||
.service(web::scope("/snapshots").configure(snapshot::configure))
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats)))
|
||||
.service(web::resource("/version").route(web::get().to(get_version)))
|
||||
.service(web::scope("/dumps").configure(dump::configure)) // done
|
||||
.service(web::scope("/snapshots").configure(snapshot::configure)) // done
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats))) // done
|
||||
.service(web::resource("/version").route(web::get().to(get_version))) // done
|
||||
.service(web::scope("/indexes").configure(indexes::configure))
|
||||
.service(web::scope("/multi-search").configure(multi_search::configure))
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure))
|
||||
.service(web::scope("/metrics").configure(metrics::configure))
|
||||
.service(web::scope("/metrics").configure(metrics::configure)) // done
|
||||
.service(web::scope("/experimental-features").configure(features::configure));
|
||||
}
|
||||
|
||||
@ -167,7 +171,8 @@ pub struct Pagination {
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct PaginationView<T> {
|
||||
pub results: Vec<T>,
|
||||
pub offset: usize,
|
||||
|
Loading…
Reference in New Issue
Block a user