mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
0.8.0
Реализованы все требуемые эндпоинты schedule. Улучшена документация.
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -50,7 +50,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"sha1",
|
"sha1 0.10.6",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -2351,7 +2351,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schedule-parser-rusted"
|
name = "schedule-parser-rusted"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-macros 0.1.0",
|
"actix-macros 0.1.0",
|
||||||
"actix-test",
|
"actix-test",
|
||||||
@@ -2367,6 +2367,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
|
"hex",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mime",
|
"mime",
|
||||||
"objectid",
|
"objectid",
|
||||||
@@ -2377,7 +2378,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"sha2",
|
"sha1 0.11.0-pre.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
"utoipa-actix-web",
|
"utoipa-actix-web",
|
||||||
@@ -2510,10 +2511,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha1"
|
||||||
version = "0.11.0-pre.5"
|
version = "0.11.0-pre.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b4241d1a56954dce82cecda5c8e9c794eef6f53abe5e5216bac0a0ea71ffa7"
|
checksum = "55f44e40722caefdd99383c25d3ae52a1094a1951215ae76f68837ece4e7f566"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ members = ["actix-macros", "actix-test"]
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "schedule-parser-rusted"
|
name = "schedule-parser-rusted"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ env_logger = "0.11.7"
|
|||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
||||||
|
hex = "0.4.3"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
objectid = "0.2.0"
|
objectid = "0.2.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
@@ -29,7 +30,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
|||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
serde_with = "3.12.0"
|
serde_with = "3.12.0"
|
||||||
serde_repr = "0.1.20"
|
serde_repr = "0.1.20"
|
||||||
sha2 = "0.11.0-pre.5"
|
sha1 = "0.11.0-pre.5"
|
||||||
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
utoipa = { version = "5", features = ["actix_extras", "chrono"] }
|
utoipa = { version = "5", features = ["actix_extras", "chrono"] }
|
||||||
|
|||||||
@@ -121,16 +121,9 @@ mod responder_json {
|
|||||||
type Body = ::actix_web::body::EitherBody<::actix_web::body::BoxBody>;
|
type Body = ::actix_web::body::EitherBody<::actix_web::body::BoxBody>;
|
||||||
|
|
||||||
fn respond_to(self, _: &::actix_web::HttpRequest) -> ::actix_web::HttpResponse<Self::Body> {
|
fn respond_to(self, _: &::actix_web::HttpRequest) -> ::actix_web::HttpResponse<Self::Body> {
|
||||||
match ::serde_json::to_string(&self) {
|
::actix_web::HttpResponse::Ok()
|
||||||
Ok(body) => ::actix_web::HttpResponse::Ok()
|
.json(self)
|
||||||
.json(body)
|
.map_into_left_body()
|
||||||
.map_into_left_body(),
|
|
||||||
|
|
||||||
Err(err) => ::actix_web::HttpResponse::from_error(
|
|
||||||
::actix_web::error::JsonPayloadError::Serialize(err),
|
|
||||||
)
|
|
||||||
.map_into_right_body(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use schedule_parser_rusted::parser::parse_xls;
|
|||||||
pub fn bench_parse_xls(c: &mut Criterion) {
|
pub fn bench_parse_xls(c: &mut Criterion) {
|
||||||
let buffer: Vec<u8> = include_bytes!("../schedule.xls").to_vec();
|
let buffer: Vec<u8> = include_bytes!("../schedule.xls").to_vec();
|
||||||
|
|
||||||
c.bench_function("parse_xls", |b| b.iter(|| parse_xls(&buffer)));
|
c.bench_function("parse_xls", |b| b.iter(|| parse_xls(&buffer).unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, bench_parse_xls);
|
criterion_group!(benches, bench_parse_xls);
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
use crate::parser::schema::ParseResult;
|
use crate::parser::schema::ParseResult;
|
||||||
|
use crate::utility::hasher::DigestHasher;
|
||||||
use crate::xls_downloader::basic_impl::BasicXlsDownloader;
|
use crate::xls_downloader::basic_impl::BasicXlsDownloader;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::hash::Hash;
|
||||||
use std::sync::{Mutex, MutexGuard};
|
use std::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Schedule {
|
pub struct Schedule {
|
||||||
pub etag: String,
|
pub etag: String,
|
||||||
|
pub fetched_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub parsed_at: DateTime<Utc>,
|
pub parsed_at: DateTime<Utc>,
|
||||||
pub data: ParseResult,
|
pub data: ParseResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Schedule {
|
||||||
|
pub fn hash(&self) -> String {
|
||||||
|
let mut hasher = DigestHasher::from(Sha1::new());
|
||||||
|
|
||||||
|
self.etag.hash(&mut hasher);
|
||||||
|
|
||||||
|
self.data.teachers.iter().for_each(|e| e.hash(&mut hasher));
|
||||||
|
self.data.groups.iter().for_each(|e| e.hash(&mut hasher));
|
||||||
|
|
||||||
|
hasher.finalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Общие данные передаваемые в эндпоинты
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub downloader: Mutex<BasicXlsDownloader>,
|
pub downloader: Mutex<BasicXlsDownloader>,
|
||||||
pub schedule: Mutex<Option<Schedule>>,
|
pub schedule: Mutex<Option<Schedule>>,
|
||||||
@@ -21,11 +39,13 @@ pub struct AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
/// Получение объекта соединения с базой данных PostgreSQL
|
||||||
pub fn connection(&self) -> MutexGuard<PgConnection> {
|
pub fn connection(&self) -> MutexGuard<PgConnection> {
|
||||||
self.database.lock().unwrap()
|
self.database.lock().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Создание нового объекта web::Data<AppState>
|
||||||
pub fn app_state() -> web::Data<AppState> {
|
pub fn app_state() -> web::Data<AppState> {
|
||||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ pub mod users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
||||||
|
let mut lock = connection.lock().unwrap();
|
||||||
|
let con = lock.deref_mut();
|
||||||
|
|
||||||
|
insert_into(users).values(user).execute(con)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
|
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
|
||||||
let mut lock = connection.lock().unwrap();
|
let mut lock = connection.lock().unwrap();
|
||||||
let con = lock.deref_mut();
|
let con = lock.deref_mut();
|
||||||
@@ -82,13 +90,7 @@ pub mod users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
#[cfg(test)]
|
||||||
let mut lock = connection.lock().unwrap();
|
|
||||||
let con = lock.deref_mut();
|
|
||||||
|
|
||||||
insert_into(users).values(user).execute(con)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
||||||
let mut lock = connection.lock().unwrap();
|
let mut lock = connection.lock().unwrap();
|
||||||
let con = lock.deref_mut();
|
let con = lock.deref_mut();
|
||||||
|
|||||||
@@ -35,12 +35,27 @@ pub enum UserRole {
|
|||||||
#[diesel(table_name = crate::database::schema::users)]
|
#[diesel(table_name = crate::database::schema::users)]
|
||||||
#[diesel(treat_none_as_null = true)]
|
#[diesel(treat_none_as_null = true)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
/// UUID аккаунта
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
|
/// Имя пользователя
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// BCrypt хеш пароля
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
|
/// Идентификатор привязанного аккаунта VK
|
||||||
pub vk_id: Option<i32>,
|
pub vk_id: Option<i32>,
|
||||||
|
|
||||||
|
/// JWT токен доступа
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
|
||||||
|
/// Группа
|
||||||
pub group: String,
|
pub group: String,
|
||||||
|
|
||||||
|
/// Роль
|
||||||
pub role: UserRole,
|
pub role: UserRole,
|
||||||
|
|
||||||
|
/// Версия установленного приложения Polytechnic+
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,29 @@ use crate::utility::jwt;
|
|||||||
use actix_macros::ResponseErrorMessage;
|
use actix_macros::ResponseErrorMessage;
|
||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::http::header;
|
||||||
use actix_web::{HttpRequest, web};
|
use actix_web::{HttpRequest, web};
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use actix_web::http::header;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, ResponseErrorMessage)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Display, ResponseErrorMessage)]
|
||||||
#[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"]
|
#[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// В запросе отсутствует заголовок Authorization
|
||||||
#[display("No Authorization header found")]
|
#[display("No Authorization header found")]
|
||||||
NoHeader,
|
NoHeader,
|
||||||
|
|
||||||
|
/// Неизвестный тип авторизации, отличающийся от Bearer
|
||||||
#[display("Bearer token is required")]
|
#[display("Bearer token is required")]
|
||||||
UnknownAuthorizationType,
|
UnknownAuthorizationType,
|
||||||
|
|
||||||
|
/// Токен не действителен
|
||||||
#[display("Invalid or expired access token")]
|
#[display("Invalid or expired access token")]
|
||||||
InvalidAccessToken,
|
InvalidAccessToken,
|
||||||
|
|
||||||
|
/// Пользователь привязанный к токену не найден в базе данных
|
||||||
#[display("No user associated with access token")]
|
#[display("No user associated with access token")]
|
||||||
NoUser,
|
NoUser,
|
||||||
}
|
}
|
||||||
@@ -35,6 +39,7 @@ impl Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Экстрактор пользователя из запроса с токеном
|
||||||
impl FromRequestSync for User {
|
impl FromRequestSync for User {
|
||||||
type Error = actix_web::Error;
|
type Error = actix_web::Error;
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,25 @@ use actix_web::{FromRequest, HttpRequest};
|
|||||||
use futures_util::future::LocalBoxFuture;
|
use futures_util::future::LocalBoxFuture;
|
||||||
use std::future::{Ready, ready};
|
use std::future::{Ready, ready};
|
||||||
|
|
||||||
pub trait FromRequestAsync: Sized {
|
/// Асинхронный экстрактор объектов из запроса
|
||||||
type Error: Into<actix_web::Error>;
|
|
||||||
|
|
||||||
async fn from_request_async(req: HttpRequest, payload: Payload) -> Result<Self, Self::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AsyncExtractor<T>(T);
|
pub struct AsyncExtractor<T>(T);
|
||||||
|
|
||||||
impl<T> AsyncExtractor<T> {
|
impl<T> AsyncExtractor<T> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Получение объекта, извлечённого с помощью экстрактора
|
||||||
pub fn into_inner(self) -> T {
|
pub fn into_inner(self) -> T {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait FromRequestAsync: Sized {
|
||||||
|
type Error: Into<actix_web::Error>;
|
||||||
|
|
||||||
|
/// Асинхронная функция для извлечения данных из запроса
|
||||||
|
async fn from_request_async(req: HttpRequest, payload: Payload) -> Result<Self, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Реализация треита FromRequest для всех асинхронных экстракторов
|
||||||
impl<T: FromRequestAsync> FromRequest for AsyncExtractor<T> {
|
impl<T: FromRequestAsync> FromRequest for AsyncExtractor<T> {
|
||||||
type Error = T::Error;
|
type Error = T::Error;
|
||||||
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||||
@@ -32,20 +37,24 @@ impl<T: FromRequestAsync> FromRequest for AsyncExtractor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FromRequestSync: Sized {
|
/// Синхронный экстрактор объектов из запроса
|
||||||
type Error: Into<actix_web::Error>;
|
|
||||||
|
|
||||||
fn from_request_sync(req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SyncExtractor<T>(T);
|
pub struct SyncExtractor<T>(T);
|
||||||
|
|
||||||
impl<T> SyncExtractor<T> {
|
impl<T> SyncExtractor<T> {
|
||||||
|
/// Получение объекта, извлечённого с помощью экстрактора
|
||||||
pub fn into_inner(self) -> T {
|
pub fn into_inner(self) -> T {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait FromRequestSync: Sized {
|
||||||
|
type Error: Into<actix_web::Error>;
|
||||||
|
|
||||||
|
/// Синхронная функция для извлечения данных из запроса
|
||||||
|
fn from_request_sync(req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Реализация треита FromRequest для всех синхронных экстракторов
|
||||||
impl<T: FromRequestSync> FromRequest for SyncExtractor<T> {
|
impl<T: FromRequestSync> FromRequest for SyncExtractor<T> {
|
||||||
type Error = T::Error;
|
type Error = T::Error;
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
|
|||||||
29
src/main.rs
29
src/main.rs
@@ -1,13 +1,19 @@
|
|||||||
use crate::app_state::{app_state, AppState};
|
use crate::app_state::{AppState, app_state};
|
||||||
use crate::middlewares::authorization::Authorization;
|
use crate::middlewares::authorization::JWTAuthorization;
|
||||||
use crate::routes::auth::sign_in::{sign_in_default, sign_in_vk};
|
use crate::routes::auth::sign_in::{sign_in_default, sign_in_vk};
|
||||||
use crate::routes::auth::sign_up::{sign_up_default, sign_up_vk};
|
use crate::routes::auth::sign_up::{sign_up_default, sign_up_vk};
|
||||||
|
use crate::routes::schedule::get_cache_status::get_cache_status;
|
||||||
|
use crate::routes::schedule::get_group::get_group;
|
||||||
|
use crate::routes::schedule::get_group_names::get_group_names;
|
||||||
|
use crate::routes::schedule::get_schedule::get_schedule;
|
||||||
|
use crate::routes::schedule::get_teacher::get_teacher;
|
||||||
|
use crate::routes::schedule::get_teacher_names::get_teacher_names;
|
||||||
|
use crate::routes::schedule::update_download_url::update_download_url;
|
||||||
use crate::routes::users::me::me;
|
use crate::routes::users::me::me;
|
||||||
use actix_web::{App, HttpServer};
|
use actix_web::{App, HttpServer};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use utoipa_actix_web::AppExt;
|
use utoipa_actix_web::AppExt;
|
||||||
use utoipa_rapidoc::RapiDoc;
|
use utoipa_rapidoc::RapiDoc;
|
||||||
use crate::routes::schedule::get_schedule::get_schedule;
|
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
|
|
||||||
@@ -31,6 +37,8 @@ async fn main() {
|
|||||||
unsafe { std::env::set_var("RUST_LOG", "debug") };
|
unsafe { std::env::set_var("RUST_LOG", "debug") };
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
let app_state = app_state();
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let auth_scope = utoipa_actix_web::scope("/auth")
|
let auth_scope = utoipa_actix_web::scope("/auth")
|
||||||
.service(sign_in_default)
|
.service(sign_in_default)
|
||||||
@@ -39,12 +47,18 @@ async fn main() {
|
|||||||
.service(sign_up_vk);
|
.service(sign_up_vk);
|
||||||
|
|
||||||
let users_scope = utoipa_actix_web::scope("/users")
|
let users_scope = utoipa_actix_web::scope("/users")
|
||||||
.wrap(Authorization)
|
.wrap(JWTAuthorization)
|
||||||
.service(me);
|
.service(me);
|
||||||
|
|
||||||
let schedule_scope = utoipa_actix_web::scope("/schedule")
|
let schedule_scope = utoipa_actix_web::scope("/schedule")
|
||||||
.wrap(Authorization)
|
.wrap(JWTAuthorization)
|
||||||
.service(get_schedule);
|
.service(get_schedule)
|
||||||
|
.service(update_download_url)
|
||||||
|
.service(get_cache_status)
|
||||||
|
.service(get_group)
|
||||||
|
.service(get_group_names)
|
||||||
|
.service(get_teacher)
|
||||||
|
.service(get_teacher_names);
|
||||||
|
|
||||||
let api_scope = utoipa_actix_web::scope("/api/v1")
|
let api_scope = utoipa_actix_web::scope("/api/v1")
|
||||||
.service(auth_scope)
|
.service(auth_scope)
|
||||||
@@ -53,7 +67,7 @@ async fn main() {
|
|||||||
|
|
||||||
let (app, api) = App::new()
|
let (app, api) = App::new()
|
||||||
.into_utoipa_app()
|
.into_utoipa_app()
|
||||||
.app_data(app_state())
|
.app_data(app_state.clone())
|
||||||
.service(api_scope)
|
.service(api_scope)
|
||||||
.split_for_parts();
|
.split_for_parts();
|
||||||
|
|
||||||
@@ -67,6 +81,7 @@ async fn main() {
|
|||||||
|
|
||||||
app.service(rapidoc_service.custom_html(patched_rapidoc_html))
|
app.service(rapidoc_service.custom_html(patched_rapidoc_html))
|
||||||
})
|
})
|
||||||
|
.workers(4)
|
||||||
.bind(("0.0.0.0", 8080))
|
.bind(("0.0.0.0", 8080))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ use actix_web::{Error, HttpRequest, ResponseError};
|
|||||||
use futures_util::future::LocalBoxFuture;
|
use futures_util::future::LocalBoxFuture;
|
||||||
use std::future::{Ready, ready};
|
use std::future::{Ready, ready};
|
||||||
|
|
||||||
pub struct Authorization;
|
/// Middleware guard работающий с токенами JWT
|
||||||
|
pub struct JWTAuthorization;
|
||||||
|
|
||||||
impl<S, B> Transform<S, ServiceRequest> for Authorization
|
impl<S, B> Transform<S, ServiceRequest> for JWTAuthorization
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
@@ -17,20 +18,21 @@ where
|
|||||||
{
|
{
|
||||||
type Response = ServiceResponse<EitherBody<B, BoxBody>>;
|
type Response = ServiceResponse<EitherBody<B, BoxBody>>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Transform = AuthorizationMiddleware<S>;
|
type Transform = JWTAuthorizationMiddleware<S>;
|
||||||
type InitError = ();
|
type InitError = ();
|
||||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
ready(Ok(AuthorizationMiddleware { service }))
|
ready(Ok(JWTAuthorizationMiddleware { service }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AuthorizationMiddleware<S> {
|
pub struct JWTAuthorizationMiddleware<S> {
|
||||||
service: S,
|
service: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, B> AuthorizationMiddleware<S>
|
/// Функция для проверки наличия и действительности токена в запросе, а так же существования пользователя к которому он привязан
|
||||||
|
impl<S, B> JWTAuthorizationMiddleware<S>
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
@@ -47,7 +49,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, B> Service<ServiceRequest> for AuthorizationMiddleware<S>
|
impl<S, B> Service<ServiceRequest> for JWTAuthorizationMiddleware<S>
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::parser::LessonParseResult::{Lessons, Street};
|
use crate::parser::LessonParseResult::{Lessons, Street};
|
||||||
use crate::parser::schema::LessonType::Break;
|
use crate::parser::schema::LessonType::Break;
|
||||||
use crate::parser::schema::{
|
use crate::parser::schema::{
|
||||||
Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry,
|
Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseError, ParseResult, ScheduleEntry,
|
||||||
};
|
};
|
||||||
use calamine::{Reader, Xls, open_workbook_from_rs};
|
use calamine::{Reader, Xls, open_workbook_from_rs};
|
||||||
use chrono::{Duration, NaiveDateTime};
|
use chrono::{Duration, NaiveDateTime};
|
||||||
@@ -14,15 +14,12 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
|
/// Данные ячейке хранящей строку
|
||||||
struct InternalId {
|
struct InternalId {
|
||||||
/**
|
/// Индекс строки
|
||||||
* Индекс строки
|
|
||||||
*/
|
|
||||||
row: u32,
|
row: u32,
|
||||||
|
|
||||||
/**
|
/// Индекс столбца
|
||||||
* Индекс столбца
|
|
||||||
*/
|
|
||||||
column: u32,
|
column: u32,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,30 +28,25 @@ struct InternalId {
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Данные о времени проведения пар из второй колонки расписания
|
||||||
struct InternalTime {
|
struct InternalTime {
|
||||||
/**
|
/// Временной отрезок проведения пары
|
||||||
* Временной отрезок проведения пары
|
|
||||||
*/
|
|
||||||
time_range: LessonTime,
|
time_range: LessonTime,
|
||||||
|
|
||||||
/**
|
/// Тип пары
|
||||||
* Тип пары
|
|
||||||
*/
|
|
||||||
lesson_type: LessonType,
|
lesson_type: LessonType,
|
||||||
|
|
||||||
/**
|
/// Индекс пары
|
||||||
* Индекс пары
|
|
||||||
*/
|
|
||||||
default_index: Option<u32>,
|
default_index: Option<u32>,
|
||||||
|
|
||||||
/**
|
/// Рамка ячейки
|
||||||
* Рамка ячейки
|
|
||||||
*/
|
|
||||||
xls_range: ((u32, u32), (u32, u32)),
|
xls_range: ((u32, u32), (u32, u32)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Сокращение типа рабочего листа
|
||||||
type WorkSheet = calamine::Range<calamine::Data>;
|
type WorkSheet = calamine::Range<calamine::Data>;
|
||||||
|
|
||||||
|
/// Получение строки из требуемой ячейки
|
||||||
fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<String> {
|
fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<String> {
|
||||||
let cell_data = if let Some(data) = worksheet.get((row as usize, col as usize)) {
|
let cell_data = if let Some(data) = worksheet.get((row as usize, col as usize)) {
|
||||||
data.to_string()
|
data.to_string()
|
||||||
@@ -82,6 +74,7 @@ fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<Str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получение границ ячейки по её верхней левой координате
|
||||||
fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32, u32), (u32, u32)) {
|
fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32, u32), (u32, u32)) {
|
||||||
let worksheet_end = worksheet.end().unwrap();
|
let worksheet_end = worksheet.end().unwrap();
|
||||||
|
|
||||||
@@ -116,7 +109,8 @@ fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32,
|
|||||||
((row, column), (row_end, column_end))
|
((row, column), (row_end, column_end))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_skeleton(worksheet: &WorkSheet) -> (Vec<InternalId>, Vec<InternalId>) {
|
/// Получение "скелета" расписания из рабочего листа
|
||||||
|
fn parse_skeleton(worksheet: &WorkSheet) -> Result<(Vec<InternalId>, Vec<InternalId>), ParseError> {
|
||||||
let range = &worksheet;
|
let range = &worksheet;
|
||||||
|
|
||||||
let mut is_parsed = false;
|
let mut is_parsed = false;
|
||||||
@@ -124,8 +118,8 @@ fn parse_skeleton(worksheet: &WorkSheet) -> (Vec<InternalId>, Vec<InternalId>) {
|
|||||||
let mut groups: Vec<InternalId> = Vec::new();
|
let mut groups: Vec<InternalId> = Vec::new();
|
||||||
let mut days: Vec<InternalId> = Vec::new();
|
let mut days: Vec<InternalId> = Vec::new();
|
||||||
|
|
||||||
let start = range.start().expect("Could not find start");
|
let start = range.start().ok_or(ParseError::UnknownWorkSheetRange)?;
|
||||||
let end = range.end().expect("Could not find end");
|
let end = range.end().ok_or(ParseError::UnknownWorkSheetRange)?;
|
||||||
|
|
||||||
let mut row = start.0;
|
let mut row = start.0;
|
||||||
while row < end.0 {
|
while row < end.0 {
|
||||||
@@ -170,15 +164,22 @@ fn parse_skeleton(worksheet: &WorkSheet) -> (Vec<InternalId>, Vec<InternalId>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(days, groups)
|
Ok((days, groups))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Результат получения пары из ячейки
|
||||||
enum LessonParseResult {
|
enum LessonParseResult {
|
||||||
|
/// Список пар длинной от одного до двух
|
||||||
|
///
|
||||||
|
/// Количество пар будет равно одному, если пара первая за день, иначе будет возвращен список из шаблона перемены и самой пары
|
||||||
Lessons(Vec<Lesson>),
|
Lessons(Vec<Lesson>),
|
||||||
|
|
||||||
|
/// Улица на которой находится корпус политехникума
|
||||||
Street(String),
|
Street(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait StringInnerSlice {
|
trait StringInnerSlice {
|
||||||
|
/// Получения отрезка строки из строки по начальному и конечному индексу
|
||||||
fn inner_slice(&self, from: usize, to: usize) -> Self;
|
fn inner_slice(&self, from: usize, to: usize) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +192,7 @@ impl StringInnerSlice for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получение нестандартного типа пары по названию
|
||||||
fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
|
fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
|
||||||
let map: HashMap<String, LessonType> = HashMap::from([
|
let map: HashMap<String, LessonType> = HashMap::from([
|
||||||
("(консультация)".to_string(), LessonType::Consultation),
|
("(консультация)".to_string(), LessonType::Consultation),
|
||||||
@@ -232,19 +234,20 @@ fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получение пары или улицы из ячейки
|
||||||
fn parse_lesson(
|
fn parse_lesson(
|
||||||
worksheet: &WorkSheet,
|
worksheet: &WorkSheet,
|
||||||
day: &mut Day,
|
day: &mut Day,
|
||||||
day_times: &Vec<InternalTime>,
|
day_times: &Vec<InternalTime>,
|
||||||
time: &InternalTime,
|
time: &InternalTime,
|
||||||
column: u32,
|
column: u32,
|
||||||
) -> LessonParseResult {
|
) -> Result<LessonParseResult, ParseError> {
|
||||||
let row = time.xls_range.0.0;
|
let row = time.xls_range.0.0;
|
||||||
|
|
||||||
let (name, lesson_type) = {
|
let (name, lesson_type) = {
|
||||||
let raw_name_opt = get_string_from_cell(&worksheet, row, column);
|
let raw_name_opt = get_string_from_cell(&worksheet, row, column);
|
||||||
if raw_name_opt.is_none() {
|
if raw_name_opt.is_none() {
|
||||||
return Lessons(Vec::new());
|
return Ok(Lessons(Vec::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw_name = raw_name_opt.unwrap();
|
let raw_name = raw_name_opt.unwrap();
|
||||||
@@ -253,7 +256,7 @@ fn parse_lesson(
|
|||||||
LazyLock::new(|| Regex::new(r"^[А-Я][а-я]+,?\s?[0-9]+$").unwrap());
|
LazyLock::new(|| Regex::new(r"^[А-Я][а-я]+,?\s?[0-9]+$").unwrap());
|
||||||
|
|
||||||
if OTHER_STREET_RE.is_match(&raw_name) {
|
if OTHER_STREET_RE.is_match(&raw_name) {
|
||||||
return Street(raw_name);
|
return Ok(Street(raw_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(guess) = guess_lesson_type(&raw_name) {
|
if let Some(guess) = guess_lesson_type(&raw_name) {
|
||||||
@@ -263,7 +266,7 @@ fn parse_lesson(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (default_range, lesson_time): (Option<[u8; 2]>, LessonTime) = {
|
let (default_range, lesson_time) = || -> Result<(Option<[u8; 2]>, LessonTime), ParseError> {
|
||||||
// check if multi-lesson
|
// check if multi-lesson
|
||||||
let cell_range = get_merge_from_start(worksheet, row, column);
|
let cell_range = get_merge_from_start(worksheet, row, column);
|
||||||
|
|
||||||
@@ -272,7 +275,7 @@ fn parse_lesson(
|
|||||||
.filter(|time| time.xls_range.1.0 == cell_range.1.0)
|
.filter(|time| time.xls_range.1.0 == cell_range.1.0)
|
||||||
.collect::<Vec<&InternalTime>>();
|
.collect::<Vec<&InternalTime>>();
|
||||||
|
|
||||||
let end_time = end_time_arr.first().expect("Unable to find lesson time!");
|
let end_time = end_time_arr.first().ok_or(ParseError::LessonTimeNotFound)?;
|
||||||
|
|
||||||
let range: Option<[u8; 2]> = if time.default_index != None {
|
let range: Option<[u8; 2]> = if time.default_index != None {
|
||||||
let default = time.default_index.unwrap() as u8;
|
let default = time.default_index.unwrap() as u8;
|
||||||
@@ -286,10 +289,10 @@ fn parse_lesson(
|
|||||||
end: end_time.time_range.end,
|
end: end_time.time_range.end,
|
||||||
};
|
};
|
||||||
|
|
||||||
(range, time)
|
Ok((range, time))
|
||||||
};
|
}()?;
|
||||||
|
|
||||||
let (name, mut subgroups) = parse_name_and_subgroups(&name);
|
let (name, mut subgroups) = parse_name_and_subgroups(&name)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let cabinets: Vec<String> = parse_cabinets(worksheet, row, column + 1);
|
let cabinets: Vec<String> = parse_cabinets(worksheet, row, column + 1);
|
||||||
@@ -345,12 +348,12 @@ fn parse_lesson(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let prev_lesson = if day.lessons.len() == 0 {
|
let prev_lesson = if day.lessons.len() == 0 {
|
||||||
return Lessons(Vec::from([lesson]));
|
return Ok(Lessons(Vec::from([lesson])));
|
||||||
} else {
|
} else {
|
||||||
&day.lessons[day.lessons.len() - 1]
|
&day.lessons[day.lessons.len() - 1]
|
||||||
};
|
};
|
||||||
|
|
||||||
Lessons(Vec::from([
|
Ok(Lessons(Vec::from([
|
||||||
Lesson {
|
Lesson {
|
||||||
lesson_type: Break,
|
lesson_type: Break,
|
||||||
default_range: None,
|
default_range: None,
|
||||||
@@ -363,9 +366,10 @@ fn parse_lesson(
|
|||||||
group: None,
|
group: None,
|
||||||
},
|
},
|
||||||
lesson,
|
lesson,
|
||||||
]))
|
])))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получение списка кабинетов справа от ячейки пары
|
||||||
fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
|
fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
|
||||||
let mut cabinets: Vec<String> = Vec::new();
|
let mut cabinets: Vec<String> = Vec::new();
|
||||||
|
|
||||||
@@ -383,15 +387,16 @@ fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
|
|||||||
cabinets
|
cabinets
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
/// Получение "чистого" названия пары и списка преподавателей из текста ячейки пары
|
||||||
|
fn parse_name_and_subgroups(name: &String) -> Result<(String, Vec<LessonSubGroup>), ParseError> {
|
||||||
static LESSON_RE: LazyLock<Regex, fn() -> Regex> =
|
static LESSON_RE: LazyLock<Regex, fn() -> Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$").unwrap());
|
LazyLock::new(|| Regex::new(r"(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$").unwrap());
|
||||||
static TEACHER_RE: LazyLock<Regex, fn() -> Regex> =
|
static TEACHER_RE: LazyLock<Regex, fn() -> Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"([А-Я][а-я]+)([А-Я])([А-Я])(?:\(([0-9])[а-я]+\))?").unwrap());
|
LazyLock::new(|| Regex::new(r"([А-Я][а-я]+)([А-Я])([А-Я])(?:\(([0-9])[а-я]+\))?").unwrap());
|
||||||
static CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
static CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"[\s.,]+").unwrap());
|
LazyLock::new(|| Regex::new(r"[\s.,]+").unwrap());
|
||||||
static NAME_CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
static END_CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"\.\s+$").unwrap());
|
LazyLock::new(|| Regex::new(r"[.\s]+$").unwrap());
|
||||||
|
|
||||||
let (teachers, lesson_name) = {
|
let (teachers, lesson_name) = {
|
||||||
let clean_name = CLEAN_RE.replace_all(&name, "").to_string();
|
let clean_name = CLEAN_RE.replace_all(&name, "").to_string();
|
||||||
@@ -402,11 +407,13 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
|||||||
let capture_name: String = capture_str.chars().take(5).collect();
|
let capture_name: String = capture_str.chars().take(5).collect();
|
||||||
|
|
||||||
(
|
(
|
||||||
NAME_CLEAN_RE.replace(&capture_str, "").to_string(),
|
END_CLEAN_RE.replace(&capture_str, "").to_string(),
|
||||||
name[0..name.find(&*capture_name).unwrap()].to_string(),
|
END_CLEAN_RE
|
||||||
|
.replace(&name[0..name.find(&*capture_name).unwrap()], "")
|
||||||
|
.to_string(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (NAME_CLEAN_RE.replace(&name, "").to_string(), Vec::new());
|
return Ok((END_CLEAN_RE.replace(&name, "").to_string(), Vec::new()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -421,7 +428,7 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
|||||||
.as_str()
|
.as_str()
|
||||||
.to_string()
|
.to_string()
|
||||||
.parse::<u8>()
|
.parse::<u8>()
|
||||||
.expect("Unable to read subgroup index!")
|
.map_err(|_| ParseError::SubgroupIndexParsingFailed)?
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
},
|
},
|
||||||
@@ -432,7 +439,7 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
|||||||
captures.get(2).unwrap().as_str().to_string(),
|
captures.get(2).unwrap().as_str().to_string(),
|
||||||
captures.get(3).unwrap().as_str().to_string()
|
captures.get(3).unwrap().as_str().to_string()
|
||||||
),
|
),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// фикс, если у кого-то отсутствует индекс подгруппы
|
// фикс, если у кого-то отсутствует индекс подгруппы
|
||||||
@@ -469,9 +476,10 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
|||||||
subgroups.reverse()
|
subgroups.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
(lesson_name, subgroups)
|
Ok((lesson_name, subgroups))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Конвертация списка пар групп в список пар преподавателей
|
||||||
fn convert_groups_to_teachers(
|
fn convert_groups_to_teachers(
|
||||||
groups: &HashMap<String, ScheduleEntry>,
|
groups: &HashMap<String, ScheduleEntry>,
|
||||||
) -> HashMap<String, ScheduleEntry> {
|
) -> HashMap<String, ScheduleEntry> {
|
||||||
@@ -537,21 +545,31 @@ fn convert_groups_to_teachers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
teachers.iter_mut().for_each(|(_, teacher)| {
|
||||||
|
teacher.days.iter_mut().for_each(|day| {
|
||||||
|
day.lessons.sort_by(|a, b| {
|
||||||
|
a.default_range.as_ref().unwrap()[1].cmp(&b.default_range.as_ref().unwrap()[1])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
teachers
|
teachers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
/// Чтение XLS документа из буфера и преобразование его в готовые к использованию расписания
|
||||||
|
pub fn parse_xls(buffer: &Vec<u8>) -> Result<ParseResult, ParseError> {
|
||||||
let cursor = Cursor::new(&buffer);
|
let cursor = Cursor::new(&buffer);
|
||||||
let mut workbook: Xls<_> = open_workbook_from_rs(cursor).expect("Can't open workbook");
|
let mut workbook: Xls<_> =
|
||||||
|
open_workbook_from_rs(cursor).map_err(|e| ParseError::BadXLS(std::sync::Arc::new(e)))?;
|
||||||
|
|
||||||
let worksheet: WorkSheet = workbook
|
let worksheet: WorkSheet = workbook
|
||||||
.worksheets()
|
.worksheets()
|
||||||
.first()
|
.first()
|
||||||
.expect("No worksheet found")
|
.ok_or(ParseError::NoWorkSheets)?
|
||||||
.1
|
.1
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
let (days_markup, groups_markup) = parse_skeleton(&worksheet);
|
let (days_markup, groups_markup) = parse_skeleton(&worksheet)?;
|
||||||
|
|
||||||
let mut groups: HashMap<String, ScheduleEntry> = HashMap::new();
|
let mut groups: HashMap<String, ScheduleEntry> = HashMap::new();
|
||||||
let mut days_times: Vec<Vec<InternalTime>> = Vec::new();
|
let mut days_times: Vec<Vec<InternalTime>> = Vec::new();
|
||||||
@@ -631,9 +649,7 @@ pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
|||||||
static TIME_RE: LazyLock<Regex, fn() -> Regex> =
|
static TIME_RE: LazyLock<Regex, fn() -> Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"(\d+\.\d+)-(\d+\.\d+)").unwrap());
|
LazyLock::new(|| Regex::new(r"(\d+\.\d+)-(\d+\.\d+)").unwrap());
|
||||||
|
|
||||||
let parse_res = TIME_RE
|
let parse_res = TIME_RE.captures(&time).ok_or(ParseError::GlobalTime)?;
|
||||||
.captures(&time)
|
|
||||||
.expect("Unable to obtain lesson start and end!");
|
|
||||||
|
|
||||||
let start_match = parse_res.get(1).unwrap().as_str();
|
let start_match = parse_res.get(1).unwrap().as_str();
|
||||||
let start_parts: Vec<&str> = start_match.split(".").collect();
|
let start_parts: Vec<&str> = start_match.split(".").collect();
|
||||||
@@ -671,7 +687,7 @@ pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
|||||||
&day_times,
|
&day_times,
|
||||||
&time,
|
&time,
|
||||||
group_markup.column,
|
group_markup.column,
|
||||||
) {
|
)? {
|
||||||
Lessons(l) => day.lessons.append(l),
|
Lessons(l) => day.lessons.append(l),
|
||||||
Street(s) => day.street = Some(s.to_owned()),
|
Street(s) => day.street = Some(s.to_owned()),
|
||||||
}
|
}
|
||||||
@@ -683,27 +699,27 @@ pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
|||||||
groups.insert(group.name.clone(), group);
|
groups.insert(group.name.clone(), group);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParseResult {
|
Ok(ParseResult {
|
||||||
teachers: convert_groups_to_teachers(&groups),
|
teachers: convert_groups_to_teachers(&groups),
|
||||||
groups,
|
groups,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn test_result() -> ParseResult {
|
pub fn test_result() -> Result<ParseResult, ParseError> {
|
||||||
let buffer: Vec<u8> = include_bytes!("../../schedule.xls").to_vec();
|
parse_xls(&include_bytes!("../../schedule.xls").to_vec())
|
||||||
|
|
||||||
parse_xls(&buffer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read() {
|
fn read() {
|
||||||
let result = test_result();
|
let result = test_result();
|
||||||
|
|
||||||
assert_ne!(result.groups.len(), 0);
|
assert!(result.is_ok());
|
||||||
assert_ne!(result.teachers.len(), 0);
|
|
||||||
|
assert_ne!(result.as_ref().unwrap().groups.len(), 0);
|
||||||
|
assert_ne!(result.as_ref().unwrap().teachers.len(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,162 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use derive_more::Display;
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
|
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct LessonTime {
|
pub struct LessonTime {
|
||||||
|
/// Начало пары
|
||||||
pub start: DateTime<Utc>,
|
pub start: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Конец пары
|
||||||
pub end: DateTime<Utc>,
|
pub end: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone, utoipa::ToSchema)]
|
#[derive(Clone, Hash, PartialEq, Debug, Serialize_repr, Deserialize_repr, ToSchema)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LessonType {
|
pub enum LessonType {
|
||||||
Default = 0, // Обычная
|
/// Обычная
|
||||||
Additional, // Допы
|
Default = 0,
|
||||||
Break, // Перемена
|
|
||||||
Consultation, // Консультация
|
/// Допы
|
||||||
IndependentWork, // Самостоятельная работа
|
Additional,
|
||||||
Exam, // Зачёт
|
|
||||||
ExamWithGrade, // Зачет с оценкой
|
/// Перемена
|
||||||
ExamDefault, // Экзамен
|
Break,
|
||||||
|
|
||||||
|
/// Консультация
|
||||||
|
Consultation,
|
||||||
|
|
||||||
|
/// Самостоятельная работа
|
||||||
|
IndependentWork,
|
||||||
|
|
||||||
|
/// Зачёт
|
||||||
|
Exam,
|
||||||
|
|
||||||
|
/// Зачет с оценкой
|
||||||
|
ExamWithGrade,
|
||||||
|
|
||||||
|
/// Экзамен
|
||||||
|
ExamDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive( Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
|
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct LessonSubGroup {
|
pub struct LessonSubGroup {
|
||||||
/**
|
/// Номер подгруппы
|
||||||
* Номер подгруппы.
|
|
||||||
*/
|
|
||||||
pub number: u8,
|
pub number: u8,
|
||||||
|
|
||||||
/**
|
/// Кабинет, если присутствует
|
||||||
* Кабинет, если присутствует.
|
|
||||||
*/
|
|
||||||
pub cabinet: Option<String>,
|
pub cabinet: Option<String>,
|
||||||
|
|
||||||
/**
|
/// Фио преподавателя
|
||||||
* Фио преподавателя.
|
|
||||||
*/
|
|
||||||
pub teacher: String,
|
pub teacher: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
|
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Lesson {
|
pub struct Lesson {
|
||||||
/**
|
/// Тип занятия
|
||||||
* Тип занятия.
|
|
||||||
*/
|
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub lesson_type: LessonType,
|
pub lesson_type: LessonType,
|
||||||
|
|
||||||
/**
|
/// Индексы пар, если присутствуют
|
||||||
* Индексы пар, если присутствуют.
|
|
||||||
*/
|
|
||||||
pub default_range: Option<[u8; 2]>,
|
pub default_range: Option<[u8; 2]>,
|
||||||
|
|
||||||
/**
|
/// Название занятия
|
||||||
* Название занятия.
|
|
||||||
*/
|
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
/**
|
/// Начало и конец занятия
|
||||||
* Начало и конец занятия.
|
|
||||||
*/
|
|
||||||
pub time: LessonTime,
|
pub time: LessonTime,
|
||||||
|
|
||||||
/**
|
/// Список подгрупп
|
||||||
* Подгруппы.
|
|
||||||
*/
|
|
||||||
#[serde(rename = "subGroups")]
|
#[serde(rename = "subGroups")]
|
||||||
pub subgroups: Option<Vec<LessonSubGroup>>,
|
pub subgroups: Option<Vec<LessonSubGroup>>,
|
||||||
|
|
||||||
/**
|
/// Группа, если это расписание для преподавателей
|
||||||
* Группа, если это расписание для преподавателей.
|
|
||||||
*/
|
|
||||||
pub group: Option<String>,
|
pub group: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, utoipa::ToSchema)]
|
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct Day {
|
pub struct Day {
|
||||||
/**
|
/// День недели
|
||||||
* День недели.
|
|
||||||
*/
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/**
|
/// Адрес другого корпуса
|
||||||
* Адрес другого корпуса.
|
|
||||||
*/
|
|
||||||
pub street: Option<String>,
|
pub street: Option<String>,
|
||||||
|
|
||||||
/**
|
/// Дата
|
||||||
* Дата.
|
|
||||||
*/
|
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
|
|
||||||
/**
|
/// Список пар в этот день
|
||||||
* Список пар в этот день.
|
|
||||||
*/
|
|
||||||
pub lessons: Vec<Lesson>,
|
pub lessons: Vec<Lesson>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, utoipa::ToSchema)]
|
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ScheduleEntry {
|
pub struct ScheduleEntry {
|
||||||
/**
|
/// Название группы или ФИО преподавателя
|
||||||
* Название группы или ФИО преподавателя.
|
|
||||||
*/
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/**
|
/// Список из шести дней
|
||||||
* Список из шести дней.
|
|
||||||
*/
|
|
||||||
pub days: Vec<Day>,
|
pub days: Vec<Day>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ParseResult {
|
pub struct ParseResult {
|
||||||
/**
|
/// Список групп
|
||||||
* Список групп.
|
|
||||||
* Ключом является название группы.
|
|
||||||
*/
|
|
||||||
pub groups: HashMap<String, ScheduleEntry>,
|
pub groups: HashMap<String, ScheduleEntry>,
|
||||||
|
|
||||||
/**
|
/// Список преподавателей
|
||||||
* Список преподавателей.
|
|
||||||
* Ключом является ФИО преподавателя.
|
|
||||||
*/
|
|
||||||
pub teachers: HashMap<String, ScheduleEntry>,
|
pub teachers: HashMap<String, ScheduleEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, ToSchema)]
|
||||||
|
pub enum ParseError {
|
||||||
|
/// Ошибки связанные с чтением XLS файла.
|
||||||
|
#[display("{}: Failed to read XLS file.", "_0")]
|
||||||
|
#[schema(value_type = String)]
|
||||||
|
BadXLS(Arc<calamine::XlsError>),
|
||||||
|
|
||||||
|
/// Не найдено ни одного листа
|
||||||
|
#[display("No work sheets found.")]
|
||||||
|
NoWorkSheets,
|
||||||
|
|
||||||
|
/// Отсутствуют данные об границах листа
|
||||||
|
#[display("There is no data on work sheet boundaries.")]
|
||||||
|
UnknownWorkSheetRange,
|
||||||
|
|
||||||
|
/// Не удалось прочитать начало и конец пары из строки
|
||||||
|
#[display("Failed to read lesson start and end times from string.")]
|
||||||
|
GlobalTime,
|
||||||
|
|
||||||
|
/// Не найдены начало и конец соответствующее паре
|
||||||
|
#[display("No start and end times matching the lesson was found.")]
|
||||||
|
LessonTimeNotFound,
|
||||||
|
|
||||||
|
/// Не удалось прочитать индекс подгруппы
|
||||||
|
#[display("Failed to read subgroup index.")]
|
||||||
|
SubgroupIndexParsingFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ParseError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
ParseError::BadXLS(_) => serializer.serialize_str("BAD_XLS"),
|
||||||
|
ParseError::NoWorkSheets => serializer.serialize_str("NO_WORK_SHEETS"),
|
||||||
|
ParseError::UnknownWorkSheetRange => {
|
||||||
|
serializer.serialize_str("UNKNOWN_WORK_SHEET_RANGE")
|
||||||
|
}
|
||||||
|
ParseError::GlobalTime => serializer.serialize_str("GLOBAL_TIME"),
|
||||||
|
ParseError::LessonTimeNotFound => serializer.serialize_str("LESSON_TIME_NOT_FOUND"),
|
||||||
|
ParseError::SubgroupIndexParsingFailed => {
|
||||||
|
serializer.serialize_str("SUBGROUP_INDEX_PARSING_FAILED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::routes::auth::shared::parse_vk_id;
|
|||||||
use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk};
|
use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk};
|
||||||
use crate::routes::schema::user::UserResponse;
|
use crate::routes::schema::user::UserResponse;
|
||||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
use crate::{AppState, utility};
|
use crate::{utility, AppState};
|
||||||
use actix_web::{post, web};
|
use actix_web::{post, web};
|
||||||
use diesel::SaveChangesDsl;
|
use diesel::SaveChangesDsl;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
@@ -55,7 +55,7 @@ async fn sign_in(
|
|||||||
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
||||||
))]
|
))]
|
||||||
#[post("/sign-in")]
|
#[post("/sign-in")]
|
||||||
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> Response {
|
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||||
sign_in(Default(data.into_inner()), &app_state).await.into()
|
sign_in(Default(data.into_inner()), &app_state).await.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>
|
|||||||
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
||||||
))]
|
))]
|
||||||
#[post("/sign-in-vk")]
|
#[post("/sign-in-vk")]
|
||||||
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
|
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||||
let data = data_json.into_inner();
|
let data = data_json.into_inner();
|
||||||
|
|
||||||
match parse_vk_id(&data.access_token) {
|
match parse_vk_id(&data.access_token) {
|
||||||
@@ -74,51 +74,57 @@ pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod schema {
|
mod schema {
|
||||||
use crate::routes::schema::PartialStatusCode;
|
|
||||||
use crate::routes::schema::user::UserResponse;
|
use crate::routes::schema::user::UserResponse;
|
||||||
use actix_macros::IntoResponseError;
|
use actix_macros::{IntoResponseError, StatusCode};
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize, Serialize, ToSchema)]
|
||||||
#[schema(as = SignIn::Request)]
|
#[schema(as = SignIn::Request)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
|
/// Имя пользователя
|
||||||
#[schema(examples("n08i40k"))]
|
#[schema(examples("n08i40k"))]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// Пароль
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod vk {
|
pub mod vk {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[schema(as = SignInVk::Request)]
|
#[schema(as = SignInVk::Request)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
|
/// Токен VK ID
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Response = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema, Clone, IntoResponseError)]
|
#[derive(Serialize, ToSchema, Clone, IntoResponseError, StatusCode)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
#[schema(as = SignIn::ErrorCode)]
|
#[schema(as = SignIn::ErrorCode)]
|
||||||
|
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||||
pub enum ErrorCode {
|
pub enum ErrorCode {
|
||||||
|
/// Некорректное имя пользователя или пароль
|
||||||
IncorrectCredentials,
|
IncorrectCredentials,
|
||||||
InvalidVkAccessToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialStatusCode for ErrorCode {
|
/// Недействительный токен VK ID
|
||||||
fn status_code(&self) -> StatusCode {
|
InvalidVkAccessToken,
|
||||||
StatusCode::NOT_ACCEPTABLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal
|
/// Internal
|
||||||
|
|
||||||
|
/// Тип авторизации
|
||||||
pub enum SignInData {
|
pub enum SignInData {
|
||||||
|
/// Имя пользователя и пароль
|
||||||
Default(Request),
|
Default(Request),
|
||||||
|
|
||||||
|
/// Идентификатор привязанного аккаунта VK
|
||||||
Vk(i32),
|
Vk(i32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +142,7 @@ mod tests {
|
|||||||
use actix_web::http::Method;
|
use actix_web::http::Method;
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::test;
|
use actix_web::test;
|
||||||
use sha2::{Digest, Sha256};
|
use sha1::{Digest, Sha1};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
async fn sign_in_client(data: Request) -> ServiceResponse {
|
async fn sign_in_client(data: Request) -> ServiceResponse {
|
||||||
@@ -152,7 +158,7 @@ mod tests {
|
|||||||
|
|
||||||
fn prepare(username: String) {
|
fn prepare(username: String) {
|
||||||
let id = {
|
let id = {
|
||||||
let mut sha = Sha256::new();
|
let mut sha = Sha1::new();
|
||||||
sha.update(&username);
|
sha.update(&username);
|
||||||
|
|
||||||
let result = sha.finalize();
|
let result = sha.finalize();
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ async fn sign_up(
|
|||||||
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
||||||
))]
|
))]
|
||||||
#[post("/sign-up")]
|
#[post("/sign-up")]
|
||||||
pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppState>) -> Response {
|
pub async fn sign_up_default(
|
||||||
|
data_json: Json<Request>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> ServiceResponse {
|
||||||
let data = data_json.into_inner();
|
let data = data_json.into_inner();
|
||||||
|
|
||||||
sign_up(
|
sign_up(
|
||||||
@@ -73,7 +76,10 @@ pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppS
|
|||||||
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
|
||||||
))]
|
))]
|
||||||
#[post("/sign-up-vk")]
|
#[post("/sign-up-vk")]
|
||||||
pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
|
pub async fn sign_up_vk(
|
||||||
|
data_json: Json<vk::Request>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> ServiceResponse {
|
||||||
let data = data_json.into_inner();
|
let data = data_json.into_inner();
|
||||||
|
|
||||||
match parse_vk_id(&data.access_token) {
|
match parse_vk_id(&data.access_token) {
|
||||||
@@ -107,11 +113,9 @@ pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppSt
|
|||||||
|
|
||||||
mod schema {
|
mod schema {
|
||||||
use crate::database::models::{User, UserRole};
|
use crate::database::models::{User, UserRole};
|
||||||
use crate::routes::schema::PartialStatusCode;
|
|
||||||
use crate::routes::schema::user::UserResponse;
|
use crate::routes::schema::user::UserResponse;
|
||||||
use crate::utility;
|
use crate::utility;
|
||||||
use actix_macros::{IntoResponseError, StatusCode};
|
use actix_macros::{IntoResponseError, StatusCode};
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use objectid::ObjectId;
|
use objectid::ObjectId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -120,16 +124,21 @@ mod schema {
|
|||||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
#[schema(as = SignUp::Request)]
|
#[schema(as = SignUp::Request)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
|
/// Имя пользователя
|
||||||
#[schema(examples("n08i40k"))]
|
#[schema(examples("n08i40k"))]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// Пароль
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
|
/// Группа
|
||||||
#[schema(examples("ИС-214/23"))]
|
#[schema(examples("ИС-214/23"))]
|
||||||
pub group: String,
|
pub group: String,
|
||||||
|
|
||||||
|
/// Роль
|
||||||
pub role: UserRole,
|
pub role: UserRole,
|
||||||
|
|
||||||
|
/// Версия установленного приложения Polytechnic+
|
||||||
#[schema(examples("3.0.0"))]
|
#[schema(examples("3.0.0"))]
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
@@ -142,43 +151,71 @@ mod schema {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[schema(as = SignUpVk::Request)]
|
#[schema(as = SignUpVk::Request)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
|
/// Токен VK ID
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
|
||||||
|
/// Имя пользователя
|
||||||
#[schema(examples("n08i40k"))]
|
#[schema(examples("n08i40k"))]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// Группа
|
||||||
#[schema(examples("ИС-214/23"))]
|
#[schema(examples("ИС-214/23"))]
|
||||||
pub group: String,
|
pub group: String,
|
||||||
|
|
||||||
|
/// Роль
|
||||||
pub role: UserRole,
|
pub role: UserRole,
|
||||||
|
|
||||||
|
/// Версия установленного приложения Polytechnic+
|
||||||
#[schema(examples("3.0.0"))]
|
#[schema(examples("3.0.0"))]
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Response = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, utoipa::ToSchema, IntoResponseError, StatusCode)]
|
#[derive(Clone, Serialize, utoipa::ToSchema, IntoResponseError, StatusCode)]
|
||||||
#[status_code = "StatusCode::NOT_ACCEPTABLE"]
|
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
#[schema(as = SignUp::ErrorCode)]
|
#[schema(as = SignUp::ErrorCode)]
|
||||||
|
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||||
pub enum ErrorCode {
|
pub enum ErrorCode {
|
||||||
|
/// Передана роль ADMIN
|
||||||
DisallowedRole,
|
DisallowedRole,
|
||||||
|
|
||||||
|
/// Неизвестное название группы
|
||||||
InvalidGroupName,
|
InvalidGroupName,
|
||||||
|
|
||||||
|
/// Пользователь с таким именем уже зарегистрирован
|
||||||
UsernameAlreadyExists,
|
UsernameAlreadyExists,
|
||||||
|
|
||||||
|
/// Недействительный токен VK ID
|
||||||
InvalidVkAccessToken,
|
InvalidVkAccessToken,
|
||||||
|
|
||||||
|
/// Пользователь с таким аккаунтом VK уже зарегистрирован
|
||||||
VkAlreadyExists,
|
VkAlreadyExists,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal
|
/// Internal
|
||||||
|
|
||||||
|
/// Данные для регистрации
|
||||||
pub struct SignUpData {
|
pub struct SignUpData {
|
||||||
|
/// Имя пользователя
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// Пароль
|
||||||
|
///
|
||||||
|
/// Должен присутствовать даже если регистрация происходит с помощью токена VK ID
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
|
/// Идентификатор аккаунта VK
|
||||||
pub vk_id: Option<i32>,
|
pub vk_id: Option<i32>,
|
||||||
|
|
||||||
|
/// Группа
|
||||||
pub group: String,
|
pub group: String,
|
||||||
|
|
||||||
|
/// Роль
|
||||||
pub role: UserRole,
|
pub role: UserRole,
|
||||||
|
|
||||||
|
/// Версия установленного приложения Polytechnic+
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
src/routes/schedule/get_cache_status.rs
Normal file
23
src/routes/schedule/get_cache_status.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use crate::routes::schedule::schema::CacheStatus;
|
||||||
|
use actix_web::{get, web};
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = CacheStatus),
|
||||||
|
))]
|
||||||
|
#[get("/cache-status")]
|
||||||
|
pub async fn get_cache_status(app_state: web::Data<AppState>) -> CacheStatus {
|
||||||
|
// Prevent thread lock
|
||||||
|
let has_schedule = app_state
|
||||||
|
.schedule
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.map(|res| res.is_some())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match has_schedule {
|
||||||
|
true => CacheStatus::from(&app_state),
|
||||||
|
false => CacheStatus::default(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
99
src/routes/schedule/get_group.rs
Normal file
99
src/routes/schedule/get_group.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use self::schema::*;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::database::models::User;
|
||||||
|
use crate::extractors::base::SyncExtractor;
|
||||||
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
|
use actix_web::{get, web};
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = Response),
|
||||||
|
(
|
||||||
|
status = SERVICE_UNAVAILABLE,
|
||||||
|
body = ResponseError<ErrorCode>,
|
||||||
|
example = json!({
|
||||||
|
"code": "NO_SCHEDULE",
|
||||||
|
"message": "Schedule not parsed yet."
|
||||||
|
})
|
||||||
|
),
|
||||||
|
(
|
||||||
|
status = NOT_FOUND,
|
||||||
|
body = ResponseError<ErrorCode>,
|
||||||
|
example = json!({
|
||||||
|
"code": "NOT_FOUND",
|
||||||
|
"message": "Required group not found."
|
||||||
|
})
|
||||||
|
),
|
||||||
|
))]
|
||||||
|
#[get("/group")]
|
||||||
|
pub async fn get_group(
|
||||||
|
user: SyncExtractor<User>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> ServiceResponse {
|
||||||
|
// Prevent thread lock
|
||||||
|
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||||
|
|
||||||
|
match schedule_lock.as_ref() {
|
||||||
|
None => ErrorCode::NoSchedule.into_response(),
|
||||||
|
Some(schedule) => match schedule.data.groups.get(&user.into_inner().group) {
|
||||||
|
None => ErrorCode::NotFound.into_response(),
|
||||||
|
Some(entry) => Ok(entry.clone().into()).into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod schema {
|
||||||
|
use crate::parser::schema::ScheduleEntry;
|
||||||
|
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use derive_more::Display;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[schema(as = GetGroup::Response)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Response {
|
||||||
|
/// Расписание группы
|
||||||
|
pub group: ScheduleEntry,
|
||||||
|
|
||||||
|
/// Устаревшая переменная
|
||||||
|
///
|
||||||
|
/// По умолчанию возвращается пустой список
|
||||||
|
#[deprecated = "Will be removed in future versions"]
|
||||||
|
pub updated: Vec<i32>,
|
||||||
|
|
||||||
|
/// Устаревшая переменная
|
||||||
|
///
|
||||||
|
/// По умолчанию начальная дата по Unix
|
||||||
|
#[deprecated = "Will be removed in future versions"]
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
impl From<ScheduleEntry> for Response {
|
||||||
|
fn from(group: ScheduleEntry) -> Self {
|
||||||
|
Self {
|
||||||
|
group,
|
||||||
|
updated: Vec::new(),
|
||||||
|
updated_at: NaiveDateTime::default().and_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[schema(as = GroupSchedule::ErrorCode)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
/// Расписания ещё не получены
|
||||||
|
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||||
|
#[display("Schedule not parsed yet.")]
|
||||||
|
NoSchedule,
|
||||||
|
|
||||||
|
/// Группа не найдена
|
||||||
|
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||||
|
#[display("Required group not found.")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/routes/schedule/get_group_names.rs
Normal file
48
src/routes/schedule/get_group_names.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use self::schema::*;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::routes::schedule::schema::ErrorCode;
|
||||||
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
|
use actix_web::{get, web};
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = Response),
|
||||||
|
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
|
||||||
|
))]
|
||||||
|
#[get("/group-names")]
|
||||||
|
pub async fn get_group_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||||
|
// Prevent thread lock
|
||||||
|
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||||
|
|
||||||
|
match schedule_lock.as_ref() {
|
||||||
|
None => ErrorCode::NoSchedule.into_response(),
|
||||||
|
Some(schedule) => {
|
||||||
|
let mut names: Vec<String> = schedule.data.groups.keys().cloned().collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
Ok(names.into()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod schema {
|
||||||
|
use crate::routes::schedule::schema::ErrorCode;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[schema(as = GetGroupNames::Response)]
|
||||||
|
pub struct Response {
|
||||||
|
/// Список названий групп отсортированный в алфавитном порядке
|
||||||
|
#[schema(examples(json!(["ИС-214/23"])))]
|
||||||
|
pub names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<String>> for Response {
|
||||||
|
fn from(names: Vec<String>) -> Self {
|
||||||
|
Self { names }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use self::schema::*;
|
use self::schema::*;
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
use crate::routes::schedule::schema::{Error, ScheduleView};
|
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
use actix_web::{get, web};
|
use actix_web::{get, web};
|
||||||
|
|
||||||
@@ -9,30 +9,17 @@ use actix_web::{get, web};
|
|||||||
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>)
|
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>)
|
||||||
))]
|
))]
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn get_schedule(app_state: web::Data<AppState>) -> Response {
|
pub async fn get_schedule(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||||
match ScheduleView::try_from(app_state.get_ref()) {
|
match ScheduleView::try_from(&app_state) {
|
||||||
Ok(res) => Ok(res).into(),
|
Ok(res) => Ok(res).into(),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
Error::NoSchedule => ErrorCode::NoSchedule.into_response(),
|
ErrorCode::NoSchedule => ErrorCode::NoSchedule.into_response(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod schema {
|
mod schema {
|
||||||
use crate::routes::schedule::schema::ScheduleView;
|
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
|
||||||
use derive_more::Display;
|
|
||||||
use serde::Serialize;
|
|
||||||
use utoipa::ToSchema;
|
|
||||||
|
|
||||||
pub type Response = crate::routes::schema::Response<ScheduleView, ErrorCode>;
|
pub type ServiceResponse = crate::routes::schema::Response<ScheduleView, ErrorCode>;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
|
||||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
||||||
#[schema(as = ScheduleView::ErrorCode)]
|
|
||||||
pub enum ErrorCode {
|
|
||||||
#[display("Schedule not parsed yet")]
|
|
||||||
NoSchedule,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/routes/schedule/get_teacher.rs
Normal file
97
src/routes/schedule/get_teacher.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use self::schema::*;
|
||||||
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
|
use crate::AppState;
|
||||||
|
use actix_web::{get, web};
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = Response),
|
||||||
|
(
|
||||||
|
status = SERVICE_UNAVAILABLE,
|
||||||
|
body = ResponseError<ErrorCode>,
|
||||||
|
example = json!({
|
||||||
|
"code": "NO_SCHEDULE",
|
||||||
|
"message": "Schedule not parsed yet."
|
||||||
|
})
|
||||||
|
),
|
||||||
|
(
|
||||||
|
status = NOT_FOUND,
|
||||||
|
body = ResponseError<ErrorCode>,
|
||||||
|
example = json!({
|
||||||
|
"code": "NOT_FOUND",
|
||||||
|
"message": "Required teacher not found."
|
||||||
|
})
|
||||||
|
),
|
||||||
|
))]
|
||||||
|
#[get("/teacher/{name}")]
|
||||||
|
pub async fn get_teacher(
|
||||||
|
name: web::Path<String>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> ServiceResponse {
|
||||||
|
// Prevent thread lock
|
||||||
|
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||||
|
|
||||||
|
match schedule_lock.as_ref() {
|
||||||
|
None => ErrorCode::NoSchedule.into_response(),
|
||||||
|
Some(schedule) => match schedule.data.teachers.get(&name.into_inner()) {
|
||||||
|
None => ErrorCode::NotFound.into_response(),
|
||||||
|
Some(entry) => Ok(entry.clone().into()).into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod schema {
|
||||||
|
use crate::parser::schema::ScheduleEntry;
|
||||||
|
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use derive_more::Display;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[schema(as = GetTeacher::Response)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Response {
|
||||||
|
/// Расписание преподавателя
|
||||||
|
pub teacher: ScheduleEntry,
|
||||||
|
|
||||||
|
/// Устаревшая переменная
|
||||||
|
///
|
||||||
|
/// По умолчанию возвращается пустой список
|
||||||
|
#[deprecated = "Will be removed in future versions"]
|
||||||
|
pub updated: Vec<i32>,
|
||||||
|
|
||||||
|
/// Устаревшая переменная
|
||||||
|
///
|
||||||
|
/// По умолчанию начальная дата по Unix
|
||||||
|
#[deprecated = "Will be removed in future versions"]
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
impl From<ScheduleEntry> for Response {
|
||||||
|
fn from(teacher: ScheduleEntry) -> Self {
|
||||||
|
Self {
|
||||||
|
teacher,
|
||||||
|
updated: Vec::new(),
|
||||||
|
updated_at: NaiveDateTime::default().and_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[schema(as = TeacherSchedule::ErrorCode)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
/// Расписания ещё не получены
|
||||||
|
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||||
|
#[display("Schedule not parsed yet.")]
|
||||||
|
NoSchedule,
|
||||||
|
|
||||||
|
/// Преподаватель не найден
|
||||||
|
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||||
|
#[display("Required teacher not found.")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/routes/schedule/get_teacher_names.rs
Normal file
48
src/routes/schedule/get_teacher_names.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use self::schema::*;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::routes::schedule::schema::ErrorCode;
|
||||||
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
|
use actix_web::{get, web};
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = Response),
|
||||||
|
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
|
||||||
|
))]
|
||||||
|
#[get("/teacher-names")]
|
||||||
|
pub async fn get_teacher_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||||
|
// Prevent thread lock
|
||||||
|
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||||
|
|
||||||
|
match schedule_lock.as_ref() {
|
||||||
|
None => ErrorCode::NoSchedule.into_response(),
|
||||||
|
Some(schedule) => {
|
||||||
|
let mut names: Vec<String> = schedule.data.teachers.keys().cloned().collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
Ok(names.into()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod schema {
|
||||||
|
use crate::routes::schedule::schema::ErrorCode;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[schema(as = GetTeacherNames::Response)]
|
||||||
|
pub struct Response {
|
||||||
|
/// Список имён преподавателей отсортированный в алфавитном порядке
|
||||||
|
#[schema(examples(json!(["Хомченко Н.Е."])))]
|
||||||
|
pub names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<String>> for Response {
|
||||||
|
fn from(names: Vec<String>) -> Self {
|
||||||
|
Self { names }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
|
pub mod get_cache_status;
|
||||||
pub mod get_schedule;
|
pub mod get_schedule;
|
||||||
|
pub mod get_group;
|
||||||
|
pub mod get_group_names;
|
||||||
|
pub mod get_teacher;
|
||||||
|
pub mod get_teacher_names;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
pub mod update_download_url;
|
||||||
|
|||||||
@@ -1,47 +1,107 @@
|
|||||||
use crate::app_state::AppState;
|
use crate::app_state::{AppState, Schedule};
|
||||||
use crate::parser::schema::ScheduleEntry;
|
use crate::parser::schema::ScheduleEntry;
|
||||||
use chrono::{DateTime, Utc};
|
use actix_macros::{IntoResponseErrorNamed, ResponderJson, StatusCode};
|
||||||
use serde::Serialize;
|
use actix_web::web;
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use derive_more::Display;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// Ответ от сервера с расписаниями
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScheduleView {
|
pub struct ScheduleView {
|
||||||
|
/// ETag расписания на сервере политехникума
|
||||||
etag: String,
|
etag: String,
|
||||||
replacer_id: Option<String>,
|
|
||||||
|
/// Дата обновления расписания на сайте политехникума
|
||||||
uploaded_at: DateTime<Utc>,
|
uploaded_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Дата последнего скачивания расписания с сервера политехникума
|
||||||
downloaded_at: DateTime<Utc>,
|
downloaded_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Расписание групп
|
||||||
groups: HashMap<String, ScheduleEntry>,
|
groups: HashMap<String, ScheduleEntry>,
|
||||||
|
|
||||||
|
/// Расписание преподавателей
|
||||||
teachers: HashMap<String, ScheduleEntry>,
|
teachers: HashMap<String, ScheduleEntry>,
|
||||||
updated_groups: Vec<Vec<i32>>,
|
|
||||||
updated_teachers: Vec<Vec<i32>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Error {
|
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||||
|
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[schema(as = ScheduleShared::ErrorCode)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
/// Расписания ещё не получены
|
||||||
|
#[display("Schedule not parsed yet.")]
|
||||||
NoSchedule,
|
NoSchedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&AppState> for ScheduleView {
|
impl TryFrom<&web::Data<AppState>> for ScheduleView {
|
||||||
type Error = Error;
|
type Error = ErrorCode;
|
||||||
|
|
||||||
fn try_from(app_state: &AppState) -> Result<Self, Self::Error> {
|
|
||||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
|
||||||
|
|
||||||
if let Some(schedule_ref) = schedule_lock.as_ref() {
|
|
||||||
let schedule = schedule_ref.clone();
|
|
||||||
|
|
||||||
|
fn try_from(app_state: &web::Data<AppState>) -> Result<Self, Self::Error> {
|
||||||
|
if let Some(schedule) = app_state.schedule.lock().unwrap().clone() {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
etag: schedule.etag,
|
etag: schedule.etag,
|
||||||
replacer_id: None,
|
|
||||||
uploaded_at: schedule.updated_at,
|
uploaded_at: schedule.updated_at,
|
||||||
downloaded_at: schedule.parsed_at,
|
downloaded_at: schedule.parsed_at,
|
||||||
groups: schedule.data.groups,
|
groups: schedule.data.groups,
|
||||||
teachers: schedule.data.teachers,
|
teachers: schedule.data.teachers,
|
||||||
updated_groups: vec![],
|
|
||||||
updated_teachers: vec![],
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NoSchedule)
|
Err(ErrorCode::NoSchedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Статус кешированного расписаний
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, ResponderJson)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CacheStatus {
|
||||||
|
/// Хеш расписаний
|
||||||
|
pub cache_hash: String,
|
||||||
|
|
||||||
|
/// Требуется ли обновить ссылку на расписание
|
||||||
|
pub cache_update_required: bool,
|
||||||
|
|
||||||
|
/// Дата последнего обновления кеша
|
||||||
|
pub last_cache_update: i64,
|
||||||
|
|
||||||
|
/// Дата обновления кешированного расписания
|
||||||
|
///
|
||||||
|
/// Определяется сервером политехникума
|
||||||
|
pub last_schedule_update: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheStatus {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
CacheStatus {
|
||||||
|
cache_hash: "0000000000000000000000000000000000000000".to_string(),
|
||||||
|
cache_update_required: true,
|
||||||
|
last_cache_update: 0,
|
||||||
|
last_schedule_update: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&web::Data<AppState>> for CacheStatus {
|
||||||
|
fn from(value: &web::Data<AppState>) -> Self {
|
||||||
|
let schedule_lock = value.schedule.lock().unwrap();
|
||||||
|
let schedule = schedule_lock.as_ref().unwrap();
|
||||||
|
|
||||||
|
CacheStatus::from(schedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Schedule> for CacheStatus {
|
||||||
|
fn from(value: &Schedule) -> Self {
|
||||||
|
Self {
|
||||||
|
cache_hash: value.hash(),
|
||||||
|
cache_update_required: (value.fetched_at - Utc::now()) > Duration::minutes(5),
|
||||||
|
last_cache_update: value.fetched_at.timestamp(),
|
||||||
|
last_schedule_update: value.updated_at.timestamp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/routes/schedule/update_download_url.rs
Normal file
132
src/routes/schedule/update_download_url.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use self::schema::*;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::app_state::Schedule;
|
||||||
|
use crate::parser::parse_xls;
|
||||||
|
use crate::routes::schedule::schema::CacheStatus;
|
||||||
|
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||||
|
use crate::xls_downloader::interface::XLSDownloader;
|
||||||
|
use actix_web::web::Json;
|
||||||
|
use actix_web::{patch, web};
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[utoipa::path(responses(
|
||||||
|
(status = OK, body = CacheStatus),
|
||||||
|
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>),
|
||||||
|
))]
|
||||||
|
#[patch("/update-download-url")]
|
||||||
|
pub async fn update_download_url(
|
||||||
|
data: Json<Request>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
) -> ServiceResponse {
|
||||||
|
if !data.url.starts_with("https://politehnikum-eng.ru/") {
|
||||||
|
return ErrorCode::NonWhitelistedHost.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut downloader = app_state.downloader.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(url) = &downloader.url {
|
||||||
|
if url.eq(&data.url) {
|
||||||
|
return Ok(CacheStatus::from(&app_state)).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match downloader.set_url(data.url.clone()).await {
|
||||||
|
Ok(fetch_result) => {
|
||||||
|
let mut schedule = app_state.schedule.lock().unwrap();
|
||||||
|
|
||||||
|
if schedule.is_some()
|
||||||
|
&& fetch_result.uploaded_at < schedule.as_ref().unwrap().updated_at
|
||||||
|
{
|
||||||
|
return ErrorCode::OutdatedSchedule.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
match downloader.fetch(false).await {
|
||||||
|
Ok(download_result) => match parse_xls(download_result.data.as_ref().unwrap()) {
|
||||||
|
Ok(data) => {
|
||||||
|
*schedule = Some(Schedule {
|
||||||
|
etag: download_result.etag,
|
||||||
|
fetched_at: download_result.requested_at,
|
||||||
|
updated_at: download_result.uploaded_at,
|
||||||
|
parsed_at: Utc::now(),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(CacheStatus::from(schedule.as_ref().unwrap())).into()
|
||||||
|
}
|
||||||
|
Err(error) => ErrorCode::InvalidSchedule(error).into_response(),
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Unknown url provided {}", data.url);
|
||||||
|
eprintln!("{:?}", error);
|
||||||
|
|
||||||
|
ErrorCode::DownloadFailed.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Unknown url provided {}", data.url);
|
||||||
|
eprintln!("{:?}", error);
|
||||||
|
|
||||||
|
ErrorCode::FetchFailed.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod schema {
|
||||||
|
use crate::parser::schema::ParseError;
|
||||||
|
use crate::routes::schedule::schema::CacheStatus;
|
||||||
|
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||||
|
use derive_more::Display;
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub type ServiceResponse = crate::routes::schema::Response<CacheStatus, ErrorCode>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct Request {
|
||||||
|
/// Ссылка на расписание
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||||
|
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||||
|
#[schema(as = SetDownloadUrl::ErrorCode)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
/// Передана ссылка с хостом отличающимся от politehnikum-eng.ru
|
||||||
|
#[display("URL with unknown host provided. Provide url with politehnikum-eng.ru host.")]
|
||||||
|
NonWhitelistedHost,
|
||||||
|
|
||||||
|
/// Не удалось получить мета-данные файла
|
||||||
|
#[display("Unable to retrieve metadata from the specified URL.")]
|
||||||
|
FetchFailed,
|
||||||
|
|
||||||
|
/// Не удалось скачать файл
|
||||||
|
#[display("Unable to retrieve data from the specified URL.")]
|
||||||
|
DownloadFailed,
|
||||||
|
|
||||||
|
/// Ссылка ведёт на устаревшее расписание
|
||||||
|
///
|
||||||
|
/// Под устаревшим расписанием подразумевается расписание, которое было опубликовано раньше, чем уже имеется на данный момент
|
||||||
|
#[display("The schedule is older than it already is.")]
|
||||||
|
OutdatedSchedule,
|
||||||
|
|
||||||
|
/// Не удалось преобразовать расписание
|
||||||
|
#[display("{}", "_0.display()")]
|
||||||
|
InvalidSchedule(ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ErrorCode {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
ErrorCode::NonWhitelistedHost => serializer.serialize_str("NON_WHITELISTED_HOST"),
|
||||||
|
ErrorCode::FetchFailed => serializer.serialize_str("FETCH_FAILED"),
|
||||||
|
ErrorCode::DownloadFailed => serializer.serialize_str("DOWNLOAD_FAILED"),
|
||||||
|
ErrorCode::OutdatedSchedule => serializer.serialize_str("OUTDATED_SCHEDULE"),
|
||||||
|
ErrorCode::InvalidSchedule(_) => serializer.serialize_str("INVALID_SCHEDULE"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,26 +113,30 @@ pub mod user {
|
|||||||
use actix_macros::ResponderJson;
|
use actix_macros::ResponderJson;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// UserResponse
|
/// Используется для скрытия чувствительных полей, таких как хеш пароля или FCM
|
||||||
///
|
|
||||||
/// Uses for stripping sensitive fields (password, fcm, etc.) from response.
|
|
||||||
#[derive(Serialize, utoipa::ToSchema, ResponderJson)]
|
#[derive(Serialize, utoipa::ToSchema, ResponderJson)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UserResponse {
|
pub struct UserResponse {
|
||||||
|
/// UUID
|
||||||
#[schema(examples("67dcc9a9507b0000772744a2"))]
|
#[schema(examples("67dcc9a9507b0000772744a2"))]
|
||||||
id: String,
|
id: String,
|
||||||
|
|
||||||
|
/// Имя пользователя
|
||||||
#[schema(examples("n08i40k"))]
|
#[schema(examples("n08i40k"))]
|
||||||
username: String,
|
username: String,
|
||||||
|
|
||||||
|
/// Группа
|
||||||
#[schema(examples("ИС-214/23"))]
|
#[schema(examples("ИС-214/23"))]
|
||||||
group: String,
|
group: String,
|
||||||
|
|
||||||
|
/// Роль
|
||||||
role: UserRole,
|
role: UserRole,
|
||||||
|
|
||||||
|
/// Идентификатор прявязанного аккаунта VK
|
||||||
#[schema(examples(498094647, json!(null)))]
|
#[schema(examples(498094647, json!(null)))]
|
||||||
vk_id: Option<i32>,
|
vk_id: Option<i32>,
|
||||||
|
|
||||||
|
/// JWT токен доступа
|
||||||
#[schema(examples(
|
#[schema(examples(
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY"
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY"
|
||||||
))]
|
))]
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
*schedule_lock = Some(Schedule {
|
*schedule_lock = Some(Schedule {
|
||||||
etag: "".to_string(),
|
etag: "".to_string(),
|
||||||
|
fetched_at: Default::default(),
|
||||||
updated_at: Default::default(),
|
updated_at: Default::default(),
|
||||||
parsed_at: Default::default(),
|
parsed_at: Default::default(),
|
||||||
data: test_result(),
|
data: test_result().unwrap(),
|
||||||
});
|
});
|
||||||
|
|
||||||
state.clone()
|
state.clone()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::fmt::{Write};
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Ответ от сервера при ошибках внутри Middleware
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ResponseErrorMessage<T: Display> {
|
pub struct ResponseErrorMessage<T: Display> {
|
||||||
code: T,
|
code: T,
|
||||||
|
|||||||
38
src/utility/hasher.rs
Normal file
38
src/utility/hasher.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use sha1::Digest;
|
||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
/// Хешер возвращающий хеш из алгоритма реализующего Digest
|
||||||
|
pub struct DigestHasher<D: Digest> {
|
||||||
|
digest: D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> DigestHasher<D>
|
||||||
|
where
|
||||||
|
D: Digest,
|
||||||
|
{
|
||||||
|
/// Получение хеша
|
||||||
|
pub fn finalize(self) -> String {
|
||||||
|
hex::encode(self.digest.finalize().0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> From<D> for DigestHasher<D>
|
||||||
|
where
|
||||||
|
D: Digest,
|
||||||
|
{
|
||||||
|
/// Создания хешера из алгоритма реализующего Digest
|
||||||
|
fn from(digest: D) -> Self {
|
||||||
|
DigestHasher { digest }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Digest> Hasher for DigestHasher<D> {
|
||||||
|
/// Заглушка для предотвращения вызова стандартного результата Hasher
|
||||||
|
fn finish(&self) -> u64 {
|
||||||
|
unimplemented!("Do not call finish()");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, bytes: &[u8]) {
|
||||||
|
self.digest.update(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,22 +9,31 @@ use std::env;
|
|||||||
use std::mem::discriminant;
|
use std::mem::discriminant;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// Ключ для верификации токена
|
||||||
static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
|
static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
|
||||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||||
|
|
||||||
DecodingKey::from_secret(secret.as_bytes())
|
DecodingKey::from_secret(secret.as_bytes())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Ключ для создания подписанного токена
|
||||||
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
|
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
|
||||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||||
|
|
||||||
EncodingKey::from_secret(secret.as_bytes())
|
EncodingKey::from_secret(secret.as_bytes())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Ошибки верификации токена
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// Токен имеет другую подпись
|
||||||
InvalidSignature,
|
InvalidSignature,
|
||||||
|
|
||||||
|
/// Ошибка чтения токена
|
||||||
InvalidToken(ErrorKind),
|
InvalidToken(ErrorKind),
|
||||||
|
|
||||||
|
/// Токен просрочен
|
||||||
Expired,
|
Expired,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,18 +43,26 @@ impl PartialEq for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Данные, которые хранит в себе токен
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
|
/// UUID аккаунта пользователя
|
||||||
id: String,
|
id: String,
|
||||||
|
|
||||||
|
/// Дата создания токена
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
iat: u64,
|
iat: u64,
|
||||||
|
|
||||||
|
/// Дата окончания действия токена
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
exp: u64,
|
exp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Алгоритм подписи токенов
|
||||||
pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
|
pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||||
|
|
||||||
|
/// Проверка токена и извлечение из него UUID аккаунта пользователя
|
||||||
pub fn verify_and_decode(token: &String) -> Result<String, Error> {
|
pub fn verify_and_decode(token: &String) -> Result<String, Error> {
|
||||||
let mut validation = Validation::new(DEFAULT_ALGORITHM);
|
let mut validation = Validation::new(DEFAULT_ALGORITHM);
|
||||||
|
|
||||||
@@ -70,6 +87,7 @@ pub fn verify_and_decode(token: &String) -> Result<String, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Создание токена пользователя
|
||||||
pub fn encode(id: &String) -> String {
|
pub fn encode(id: &String) -> String {
|
||||||
let header = Header {
|
let header = Header {
|
||||||
typ: Some(String::from("JWT")),
|
typ: Some(String::from("JWT")),
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod hasher;
|
||||||
@@ -2,7 +2,7 @@ use crate::xls_downloader::interface::{FetchError, FetchOk, FetchResult, XLSDown
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
pub struct BasicXlsDownloader {
|
pub struct BasicXlsDownloader {
|
||||||
url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_specified(url: &String, user_agent: String, head: bool) -> FetchResult {
|
async fn fetch_specified(url: &String, user_agent: String, head: bool) -> FetchResult {
|
||||||
@@ -73,14 +73,14 @@ impl XLSDownloader for BasicXlsDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_url(&mut self, url: String) -> Result<(), FetchError> {
|
async fn set_url(&mut self, url: String) -> FetchResult {
|
||||||
let result = fetch_specified(&url, "t.me/polytechnic_next".to_string(), true).await;
|
let result = fetch_specified(&url, "t.me/polytechnic_next".to_string(), true).await;
|
||||||
|
|
||||||
if let Ok(_) = result {
|
if let Ok(_) = result {
|
||||||
Ok(self.url = Some(url))
|
self.url = Some(url);
|
||||||
} else {
|
|
||||||
Err(result.err().unwrap())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// Ошибки получения данных XLS
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub enum FetchError {
|
pub enum FetchError {
|
||||||
|
/// Не установлена ссылка на файл
|
||||||
NoUrlProvided,
|
NoUrlProvided,
|
||||||
|
|
||||||
|
/// Неизвестная ошибка
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
||||||
|
/// Сервер вернул статус код отличающийся от 200
|
||||||
BadStatusCode,
|
BadStatusCode,
|
||||||
|
|
||||||
|
/// Ссылка ведёт на файл другого типа
|
||||||
BadContentType,
|
BadContentType,
|
||||||
|
|
||||||
|
/// Сервер не вернул ожидаемые заголовки
|
||||||
BadHeaders,
|
BadHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Результат получения данных XLS
|
||||||
pub struct FetchOk {
|
pub struct FetchOk {
|
||||||
|
/// ETag объекта
|
||||||
pub etag: String,
|
pub etag: String,
|
||||||
|
|
||||||
|
/// Дата загрузки файла
|
||||||
pub uploaded_at: DateTime<Utc>,
|
pub uploaded_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Дата получения данных
|
||||||
pub requested_at: DateTime<Utc>,
|
pub requested_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Данные файла
|
||||||
pub data: Option<Vec<u8>>,
|
pub data: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FetchOk {
|
impl FetchOk {
|
||||||
|
/// Результат без контента файла
|
||||||
pub fn head(etag: String, uploaded_at: DateTime<Utc>) -> Self {
|
pub fn head(etag: String, uploaded_at: DateTime<Utc>) -> Self {
|
||||||
FetchOk {
|
FetchOk {
|
||||||
etag,
|
etag,
|
||||||
@@ -26,6 +45,7 @@ impl FetchOk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Полный результат
|
||||||
pub fn get(etag: String, uploaded_at: DateTime<Utc>, data: Vec<u8>) -> Self {
|
pub fn get(etag: String, uploaded_at: DateTime<Utc>, data: Vec<u8>) -> Self {
|
||||||
FetchOk {
|
FetchOk {
|
||||||
etag,
|
etag,
|
||||||
@@ -39,6 +59,9 @@ impl FetchOk {
|
|||||||
pub type FetchResult = Result<FetchOk, FetchError>;
|
pub type FetchResult = Result<FetchOk, FetchError>;
|
||||||
|
|
||||||
pub trait XLSDownloader {
|
pub trait XLSDownloader {
|
||||||
|
/// Получение данных о файле, и, опционально, его контент
|
||||||
async fn fetch(&self, head: bool) -> FetchResult;
|
async fn fetch(&self, head: bool) -> FetchResult;
|
||||||
async fn set_url(&mut self, url: String) -> Result<(), FetchError>;
|
|
||||||
|
/// Установка ссылки на файл
|
||||||
|
async fn set_url(&mut self, url: String) -> FetchResult;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user