docs: documented most of the code
This commit is contained in:
parent
e367930505
commit
e4287bb2ac
18 changed files with 223 additions and 191 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -984,6 +984,37 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iotishnik-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
|
@ -1156,37 +1187,6 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
|
||||
[[package]]
|
||||
name = "narodmon-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "narodmon-server"
|
||||
name = "iotishnik-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# IoTishnik
|
||||
Платформа для обработки данных окружающей среды с IoT устройств.
|
||||
|
||||
## Описание идеи
|
||||
> ### Что есть сейчас:
|
||||
>
|
||||
> Есть куча девайсов, которые сейчас со своих сенсоров, для публичного и в некоторых случаях частного использования выкладывают на сервер narodmon.ru данные.
|
||||
>
|
||||
> ### В чём проблема narodmon.ru:
|
||||
>
|
||||
>1. Там всё такоееее легаси, что ты не представляешь. Речь идёт о cgi скриптах написанных на bash.
|
||||
>2. Разрабы ищут помощь в виде лиц, в которых можно плевать без последствий сколько угодно.
|
||||
>3. Отсутствие тех поддержки для обычных пользователей от слову совсем.
|
||||
>4. Ужасное API которое ужасное не только из-за своей кривизны. В нём нужно отправлять телеметрию устройств, админ панель разработчика считай отсутствует. О OAuth2 вообще говорить не стоит. И если ты отправишь неправильные запросы с клиента своего, твой "api ключ" приложения заблокируют, и пофиг на то, что его можно спиздить с чужих приложений.
|
||||
>5. Интерфейс просто ужас. Можешь сам зайти и посмотреть, даже писать про это не буду. Даже сам UX в изоляции от UI ужасный.
|
||||
>6. Документация API не соблюдается. Даже когда мы с Андреем написали, что, мол, у вас поля изменились, им было плевать.
|
||||
>7. Работа с приватными датчиками ужастная и требует денег для того, чтобы оно вообще работало.
|
||||
>8. Некоторые очень важные API для устройств (такие как MQTT, на которых работают большое количество готовых продуктов не заточенные под narodmon.ru) доступны тоже только по подписке разрабам.
|
||||
>
|
||||
> ### Наше решение:
|
||||
>
|
||||
> Сделать с нуля своё решение, которое будет горизонтально масштабируемое, с поддержкой старых API для поддержки устройств сделанных под narodmon.ru . Выдать разрабам которые хотят новое и мощное API это API вместе с SDK в виде либ под ардуинку и т.п. Сделать поддержку OAuth для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть
|
|
@ -1,3 +1,5 @@
|
|||
//! Модуль для парсинга всего, что связанно с данными от устройств.
|
||||
|
||||
pub mod error;
|
||||
mod packet_types;
|
||||
pub mod parser;
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
use crate::hashes::SupportedUnit;
|
||||
//! Сборник типов для внутренней обработки данных с датчиков.
|
||||
//!
|
||||
//! Предполагается, что struct-ы будут совместимы с JSON API для передачи данных датчиков.
|
||||
|
||||
// Не забывайте про:
|
||||
// #[serde(rename = "...")]
|
||||
// #[serde(alias = "...")]
|
||||
|
||||
use crate::utils::SupportedUnit;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
|
||||
|
@ -11,6 +19,17 @@ use serde_with::serde_as;
|
|||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Данные с одного датчика.
|
||||
///
|
||||
/// Основной идентификатор - поле `mac`. В случае с [crate::web_server::old_device_sensor_api],
|
||||
/// `mac` может быть не полем, а названием ключа в параметрах. Из-за двусмысленности документации
|
||||
/// NarodMon, `mac` может означать EUI-48 совместимый MAC адрес, или же просто
|
||||
/// уникальный идентификатор.
|
||||
///
|
||||
/// Парсинг этих данных отличается в разных транспортных протоколах.
|
||||
/// Для HTTP /post или /get: см. [crate::web_server::old_device_sensor_api]
|
||||
/// Для TCP/UDP: TODO
|
||||
/// Для MQTT: TODO
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SensorValue {
|
||||
pub mac: String,
|
||||
|
@ -26,14 +45,9 @@ impl Hash for SensorValue {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DashSeparator {}
|
||||
|
||||
impl Separator for DashSeparator {
|
||||
fn separator() -> &'static str {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
|
||||
/// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with].
|
||||
///
|
||||
/// Преобразует MAC-адрес.
|
||||
fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> {
|
||||
Ok(parse_mac_address(value)?.1)
|
||||
}
|
||||
|
|
60
src/main.rs
60
src/main.rs
|
@ -1,69 +1,13 @@
|
|||
/*
|
||||
Три датчика реалтайм:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93
|
||||
#H1#21
|
||||
#P1#700.91
|
||||
##
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
Формат пакета данных:
|
||||
#MAC[#NAME]\n
|
||||
#mac1#value1[#time1][#name1]\n
|
||||
...
|
||||
#macN#valueN[#timeN][#nameN]\n
|
||||
##
|
||||
|
||||
Загрузка истории показаний:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93#1687006667
|
||||
#T1#10.17#1687006067
|
||||
#T1#27.26#1687005467
|
||||
##
|
||||
|
||||
C названием и координатами:
|
||||
#26-94-1D-75-C2-F8#Метео
|
||||
#OWNER#nm17
|
||||
#T1#6.93#Улица
|
||||
#T2#27.26#Дом
|
||||
#P1#700.91#Барометр
|
||||
#LAT#54.308997
|
||||
#LON#48.395861
|
||||
#ALT#233
|
||||
##
|
||||
*/
|
||||
#![feature(try_blocks)]
|
||||
extern crate core;
|
||||
|
||||
mod hashes;
|
||||
mod utils;
|
||||
mod ingest_protocol;
|
||||
mod web_server;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use crate::web_server::server_main;
|
||||
|
||||
|
||||
/*fn parse_sensor_value(input: Vec<&str>) -> MyIError<Vec<&str>, NarodMonValues> {
|
||||
Ok(
|
||||
(input, NarodMonValues {
|
||||
mac: Default::default(),
|
||||
value: Default::default(),
|
||||
time: None,
|
||||
name: None,
|
||||
})
|
||||
)
|
||||
}*/
|
||||
|
||||
struct Params {}
|
||||
|
||||
#[ntex::main]
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
//! Глобальный модуль для вспомогательных типов и утилит.
|
||||
//!
|
||||
|
||||
|
||||
use phf::phf_map;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
/// Поддерживаемые типы.
|
||||
///
|
||||
/// TODO: Решить необходимо ли к данным прикреплять единицы измерения.
|
||||
/// TODO: Сейчас вообще сомнительно оставлять ли это или нет.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum SupportedUnit {
|
||||
Celsius, // Needs verification > 273.15
|
||||
Celsius, // Needs verification > -273.15
|
||||
Percentage, // Needs verification >= 100 && <= 0
|
||||
MillimeterHg, // Needs verification
|
||||
UVIndex, // Needs verification
|
||||
|
@ -56,6 +65,7 @@ impl<'de> Deserialize<'de> for SupportedUnit {
|
|||
}
|
||||
}
|
||||
|
||||
/// Таблица преобразования текстового представления единиц в значения [SupportedUnit].
|
||||
static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
|
||||
"C" => SupportedUnit::Celsius,
|
||||
"%" => SupportedUnit::Percentage,
|
|
@ -12,11 +12,14 @@ use thiserror::Error;
|
|||
|
||||
|
||||
use crate::insert_header;
|
||||
use crate::web_server::old_device_sensor_api::QSParserError;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
/// Главный объект ошибки [std::error::Error] для всего Web API.
|
||||
///
|
||||
/// В целом, все Result у Web сервера должны использовать этот Error.
|
||||
#[derive(Debug, Error, Display)]
|
||||
pub enum AppError {
|
||||
#[display(fmt = "IDK")]
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
use crate::web_server::old_app_api::old_api_handler;
|
||||
//! Модуль веб сервера.
|
||||
//!
|
||||
//! Все модули отвечают только за Web сторону. Такие вещи как
|
||||
//! [crate::web_server::old_device_sensor_api] отвечают только за веб версию.
|
||||
//!
|
||||
//! TODO: Начать работу над TCP/UDP и MQTT сервером
|
||||
|
||||
use old_app_api::old_api_handler;
|
||||
use fred::bytes_utils::Str;
|
||||
use fred::prelude::*;
|
||||
|
||||
|
||||
|
||||
pub(crate) mod app_error;
|
||||
pub mod app_error;
|
||||
pub mod old_app_api;
|
||||
mod old_device_sensor_api;
|
||||
pub mod old_device_sensor_api;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
use crate::web_server::NMAppState;
|
||||
use nom::AsBytes;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use ntex::web::types::State;
|
||||
|
||||
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
|
@ -1,7 +1,36 @@
|
|||
mod methods;
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
|
||||
|
||||
pub use methods::*;
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,53 @@
|
|||
mod config_app;
|
||||
//! Модуль обработки данных для приложений конечных пользователей, которые используют старый API.
|
||||
|
||||
mod handlers;
|
||||
mod types;
|
||||
|
||||
pub use config_app::old_api_handler;
|
||||
use ntex::web::types::State;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use nom::AsBytes;
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
|
||||
|
||||
/// Обработчик запросов от приложений.
|
||||
///
|
||||
/// Отвечает за разделение на функции по `cmd`.
|
||||
///
|
||||
/// Вызывается напрямую из ntex приложения.
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -24,6 +23,11 @@ pub struct AddLikeRequest {
|
|||
pub version: u64,
|
||||
}
|
||||
|
||||
/// Обязательные параметры у JSON app API.
|
||||
///
|
||||
/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается
|
||||
/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из
|
||||
/// этой структуры в каждом специфичном типе.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MandatoryParams<'a> {
|
||||
#[serde(borrow)]
|
||||
|
@ -32,6 +36,11 @@ pub struct MandatoryParams<'a> {
|
|||
#[serde(borrow)]
|
||||
pub lang: Cow<'a, str>,
|
||||
|
||||
/// Уникальный ID клиента.
|
||||
///
|
||||
/// Используется на подобии как куки PHPSESSID в php.
|
||||
///
|
||||
/// См. также: <https://www.php.net/manual/en/book.session.php>
|
||||
#[serde(borrow)]
|
||||
pub uuid: Cow<'a, str>,
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
mod qs_parser;
|
||||
//! Модуль обработки данных с устройств, которые используют старый API.
|
||||
|
||||
pub mod qs_parser;
|
||||
|
||||
use std::str::FromStr;
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket};
|
||||
|
@ -13,6 +15,7 @@ use hifitime::Epoch;
|
|||
use thiserror::Error;
|
||||
use ufmt::uwrite;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -20,9 +23,17 @@ pub enum Error {
|
|||
DeviceNotFound(String),
|
||||
#[error("Time sent with the device is way to behind now")]
|
||||
TimeIsLongBehindNow,
|
||||
#[error("{0}")]
|
||||
QSParserError(#[from] QSParserError)
|
||||
}
|
||||
|
||||
|
||||
/// Обработчик данных датчиков с устройств.
|
||||
///
|
||||
/// Слушает /post и /get.
|
||||
/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET.
|
||||
///
|
||||
/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String.
|
||||
pub async fn device_handler<'a>(
|
||||
request: web::HttpRequest,
|
||||
body: Bytes,
|
||||
|
|
|
@ -40,6 +40,11 @@ impl From<serde_qs::Error> for QSParserError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков
|
||||
/// [SensorValue].
|
||||
///
|
||||
/// Формат: `<SENSOR_MAC>=<SENSOR_VALUE>`.
|
||||
/// Других данных на подобии названия и времени нет.
|
||||
pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> {
|
||||
let mut hashset = HashSet::new();
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
|
||||
//! неспециализированные.
|
||||
|
||||
pub mod redis;
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Сборник утилит для работы с Redis.
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use fred::prelude::*;
|
||||
use heapless::String as HeaplessString;
|
||||
|
@ -6,13 +8,17 @@ use regex::Regex;
|
|||
use ufmt::uwrite;
|
||||
|
||||
lazy_static! {
|
||||
/// Разрешённые знаки для API ключа.
|
||||
static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap();
|
||||
}
|
||||
|
||||
/// Описание полей в KV DB у `apikey_{}`.
|
||||
pub struct ApiKeyDescription {
|
||||
/// ID владельца API ключа.
|
||||
apikey_owner: i64,
|
||||
}
|
||||
|
||||
/// Проверка API ключа на валидность.
|
||||
pub async fn is_api_key_valid(
|
||||
client: &RedisClient,
|
||||
api_key: &str,
|
||||
|
|
Loading…
Add table
Reference in a new issue