mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 17:57:47 +03:00
feat!: add telegram auth and async refactor
- Removed "/schedule/update-download-url" endpoint, this mechanism was replaced by Yandex Cloud FaaS. Ура :) - Improved schedule caching mechanism. - Added Telegram WebApp authentication support. - Reworked endpoints responses and errors mechanism. - Refactored application state management. - Make synchronous database operations, middlewares and extractors to asynchronous. - Made user password field optional to support multiple auth methods. - Renamed users table column "version" to "android_version" and made it nullable.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
mod shared;
|
||||
mod sign_in;
|
||||
mod sign_up;
|
||||
mod shared;
|
||||
|
||||
pub use sign_in::*;
|
||||
pub use sign_up::*;
|
||||
|
||||
// TODO: change-password
|
||||
// TODO: change-password
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use jsonwebtoken::errors::ErrorKind;
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use self::schema::*;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::User;
|
||||
use crate::database::driver::users::UserSave;
|
||||
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, VkOAuth};
|
||||
use crate::routes::schema::ResponseError;
|
||||
use crate::routes::schema::user::UserResponse;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::utility::mutex::MutexScope;
|
||||
use crate::{AppState, utility};
|
||||
use actix_web::{post, web};
|
||||
use diesel::SaveChangesDsl;
|
||||
use web::Json;
|
||||
|
||||
async fn sign_in_combined(
|
||||
@@ -16,14 +14,18 @@ async fn sign_in_combined(
|
||||
app_state: &web::Data<AppState>,
|
||||
) -> Result<UserResponse, ErrorCode> {
|
||||
let user = match &data {
|
||||
Default(data) => driver::users::get_by_username(&app_state, &data.username),
|
||||
Vk(id) => driver::users::get_by_vk_id(&app_state, *id),
|
||||
Default(data) => driver::users::get_by_username(&app_state, &data.username).await,
|
||||
VkOAuth(id) => driver::users::get_by_vk_id(&app_state, *id).await,
|
||||
};
|
||||
|
||||
match user {
|
||||
Ok(mut user) => {
|
||||
if let Default(data) = data {
|
||||
match bcrypt::verify(&data.password, &user.password) {
|
||||
if user.password.is_none() {
|
||||
return Err(ErrorCode::IncorrectCredentials);
|
||||
}
|
||||
|
||||
match bcrypt::verify(&data.password, &user.password.as_ref().unwrap()) {
|
||||
Ok(result) => {
|
||||
if !result {
|
||||
return Err(ErrorCode::IncorrectCredentials);
|
||||
@@ -35,12 +37,9 @@ async fn sign_in_combined(
|
||||
}
|
||||
}
|
||||
|
||||
user.access_token = utility::jwt::encode(&user.id);
|
||||
user.access_token = Some(utility::jwt::encode(&user.id));
|
||||
|
||||
app_state.database.scope(|conn| {
|
||||
user.save_changes::<User>(conn)
|
||||
.expect("Failed to update user")
|
||||
});
|
||||
user.save(&app_state).await.expect("Failed to update user");
|
||||
|
||||
Ok(user.into())
|
||||
}
|
||||
@@ -71,15 +70,17 @@ pub async fn sign_in_vk(
|
||||
) -> ServiceResponse {
|
||||
let data = data_json.into_inner();
|
||||
|
||||
match parse_vk_id(&data.access_token, app_state.vk_id.client_id) {
|
||||
Ok(id) => sign_in_combined(Vk(id), &app_state).await.into(),
|
||||
Err(_) => ErrorCode::InvalidVkAccessToken.into_response(),
|
||||
match parse_vk_id(&data.access_token, app_state.get_env().vk_id.client_id) {
|
||||
Ok(id) => sign_in_combined(VkOAuth(id), &app_state).await,
|
||||
Err(_) => Err(ErrorCode::InvalidVkAccessToken),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schema::user::UserResponse;
|
||||
use actix_macros::{IntoResponseError, StatusCode};
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -109,15 +110,17 @@ mod schema {
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone, IntoResponseError, StatusCode)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[schema(as = SignIn::ErrorCode)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||
pub enum ErrorCode {
|
||||
/// Incorrect username or password.
|
||||
#[display("Incorrect username or password.")]
|
||||
IncorrectCredentials,
|
||||
|
||||
/// Invalid VK ID token.
|
||||
#[display("Invalid VK ID token.")]
|
||||
InvalidVkAccessToken,
|
||||
}
|
||||
|
||||
@@ -129,7 +132,7 @@ mod schema {
|
||||
Default(Request),
|
||||
|
||||
/// Identifier of the attached account VK.
|
||||
Vk(i32),
|
||||
VkOAuth(i32),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +153,7 @@ mod tests {
|
||||
use std::fmt::Write;
|
||||
|
||||
async fn sign_in_client(data: Request) -> ServiceResponse {
|
||||
let app = test_app(test_app_state(Default::default()).await, sign_in).await;
|
||||
let app = test_app(test_app_state().await, sign_in).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-in")
|
||||
.method(Method::POST)
|
||||
@@ -184,14 +187,16 @@ mod tests {
|
||||
&User {
|
||||
id: id.clone(),
|
||||
username,
|
||||
password: bcrypt::hash("example".to_string(), bcrypt::DEFAULT_COST).unwrap(),
|
||||
password: Some(bcrypt::hash("example".to_string(), bcrypt::DEFAULT_COST).unwrap()),
|
||||
vk_id: None,
|
||||
access_token: utility::jwt::encode(&id),
|
||||
group: "ИС-214/23".to_string(),
|
||||
telegram_id: None,
|
||||
access_token: Some(utility::jwt::encode(&id)),
|
||||
group: Some("ИС-214/23".to_string()),
|
||||
role: UserRole::Student,
|
||||
version: "1.0.0".to_string(),
|
||||
android_version: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::UserRole;
|
||||
use crate::routes::auth::shared::{Error, parse_vk_id};
|
||||
use crate::routes::auth::shared::parse_vk_id;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use crate::routes::schema::user::UserResponse;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use actix_web::{post, web};
|
||||
use rand::{Rng, rng};
|
||||
use web::Json;
|
||||
|
||||
async fn sign_up_combined(
|
||||
@@ -18,29 +17,30 @@ async fn sign_up_combined(
|
||||
return Err(ErrorCode::DisallowedRole);
|
||||
}
|
||||
|
||||
// If specified group doesn't exist in schedule.
|
||||
let schedule_opt = app_state.schedule.lock().unwrap();
|
||||
|
||||
if let Some(schedule) = &*schedule_opt {
|
||||
if !schedule.data.groups.contains_key(&data.group) {
|
||||
return Err(ErrorCode::InvalidGroupName);
|
||||
}
|
||||
if !app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.contains_key(&data.group)
|
||||
{
|
||||
return Err(ErrorCode::InvalidGroupName);
|
||||
}
|
||||
|
||||
// If user with specified username already exists.
|
||||
if driver::users::contains_by_username(&app_state, &data.username) {
|
||||
if driver::users::contains_by_username(&app_state, &data.username).await {
|
||||
return Err(ErrorCode::UsernameAlreadyExists);
|
||||
}
|
||||
|
||||
// If user with specified VKID already exists.
|
||||
if let Some(id) = data.vk_id {
|
||||
if driver::users::contains_by_vk_id(&app_state, id) {
|
||||
if driver::users::contains_by_vk_id(&app_state, id).await {
|
||||
return Err(ErrorCode::VkAlreadyExists);
|
||||
}
|
||||
}
|
||||
|
||||
let user = data.into();
|
||||
driver::users::insert(&app_state, &user).unwrap();
|
||||
driver::users::insert(&app_state, &user).await.unwrap();
|
||||
|
||||
Ok(UserResponse::from(&user)).into()
|
||||
}
|
||||
@@ -56,7 +56,7 @@ pub async fn sign_up(data_json: Json<Request>, app_state: web::Data<AppState>) -
|
||||
sign_up_combined(
|
||||
SignUpData {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
password: Some(data.password),
|
||||
vk_id: None,
|
||||
group: data.group,
|
||||
role: data.role,
|
||||
@@ -79,40 +79,32 @@ pub async fn sign_up_vk(
|
||||
) -> ServiceResponse {
|
||||
let data = data_json.into_inner();
|
||||
|
||||
match parse_vk_id(&data.access_token, app_state.vk_id.client_id) {
|
||||
Ok(id) => sign_up_combined(
|
||||
SignUpData {
|
||||
username: data.username,
|
||||
password: rng()
|
||||
.sample_iter(&rand::distr::Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect(),
|
||||
vk_id: Some(id),
|
||||
group: data.group,
|
||||
role: data.role,
|
||||
version: data.version,
|
||||
},
|
||||
&app_state,
|
||||
)
|
||||
.await
|
||||
.into(),
|
||||
Err(err) => {
|
||||
if err != Error::Expired {
|
||||
eprintln!("Failed to parse vk id token!");
|
||||
eprintln!("{:?}", err);
|
||||
}
|
||||
|
||||
ErrorCode::InvalidVkAccessToken.into_response()
|
||||
match parse_vk_id(&data.access_token, app_state.get_env().vk_id.client_id) {
|
||||
Ok(id) => {
|
||||
sign_up_combined(
|
||||
SignUpData {
|
||||
username: data.username,
|
||||
password: None,
|
||||
vk_id: Some(id),
|
||||
group: data.group,
|
||||
role: data.role,
|
||||
version: data.version,
|
||||
},
|
||||
&app_state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(_) => Err(ErrorCode::InvalidVkAccessToken),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::schema::user::UserResponse;
|
||||
use crate::utility;
|
||||
use actix_macros::{IntoResponseError, StatusCode};
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use objectid::ObjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -170,24 +162,29 @@ mod schema {
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
|
||||
|
||||
#[derive(Clone, Serialize, utoipa::ToSchema, IntoResponseError, StatusCode)]
|
||||
#[derive(Clone, Serialize, Display, utoipa::ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = SignUp::ErrorCode)]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||
pub enum ErrorCode {
|
||||
/// Conveyed the role of Admin.
|
||||
#[display("Conveyed the role of Admin.")]
|
||||
DisallowedRole,
|
||||
|
||||
/// Unknown name of the group.
|
||||
#[display("Unknown name of the group.")]
|
||||
InvalidGroupName,
|
||||
|
||||
/// User with this name is already registered.
|
||||
#[display("User with this name is already registered.")]
|
||||
UsernameAlreadyExists,
|
||||
|
||||
/// Invalid VK ID token.
|
||||
#[display("Invalid VK ID token.")]
|
||||
InvalidVkAccessToken,
|
||||
|
||||
/// User with such an account VK is already registered.
|
||||
#[display("User with such an account VK is already registered.")]
|
||||
VkAlreadyExists,
|
||||
}
|
||||
|
||||
@@ -195,13 +192,14 @@ mod schema {
|
||||
|
||||
/// Data for registration.
|
||||
pub struct SignUpData {
|
||||
// TODO: сделать ограничение на минимальную и максимальную длину при регистрации и смене.
|
||||
/// User name.
|
||||
pub username: String,
|
||||
|
||||
/// Password.
|
||||
///
|
||||
/// Should be present even if registration occurs using the VK ID token.
|
||||
pub password: String,
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Account identifier VK.
|
||||
pub vk_id: Option<i32>,
|
||||
@@ -218,18 +216,23 @@ mod schema {
|
||||
|
||||
impl Into<User> for SignUpData {
|
||||
fn into(self) -> User {
|
||||
assert_ne!(self.password.is_some(), self.vk_id.is_some());
|
||||
|
||||
let id = ObjectId::new().unwrap().to_string();
|
||||
let access_token = utility::jwt::encode(&id);
|
||||
let access_token = Some(utility::jwt::encode(&id));
|
||||
|
||||
User {
|
||||
id,
|
||||
username: self.username,
|
||||
password: bcrypt::hash(self.password, bcrypt::DEFAULT_COST).unwrap(),
|
||||
password: self
|
||||
.password
|
||||
.map(|x| bcrypt::hash(x, bcrypt::DEFAULT_COST).unwrap()),
|
||||
vk_id: self.vk_id,
|
||||
telegram_id: None,
|
||||
access_token,
|
||||
group: self.group,
|
||||
group: Some(self.group),
|
||||
role: self.role,
|
||||
version: self.version,
|
||||
android_version: Some(self.version),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,42 +244,28 @@ mod tests {
|
||||
use crate::database::models::UserRole;
|
||||
use crate::routes::auth::sign_up::schema::Request;
|
||||
use crate::routes::auth::sign_up::sign_up;
|
||||
use crate::test_env::tests::{
|
||||
TestAppStateParams, TestScheduleType, static_app_state, test_app_state, test_env,
|
||||
};
|
||||
use crate::test_env::tests::{static_app_state, test_app_state, test_env};
|
||||
use actix_test::test_app;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
struct SignUpPartial {
|
||||
username: String,
|
||||
group: String,
|
||||
struct SignUpPartial<'a> {
|
||||
username: &'a str,
|
||||
group: &'a str,
|
||||
role: UserRole,
|
||||
load_schedule: bool,
|
||||
}
|
||||
|
||||
async fn sign_up_client(data: SignUpPartial) -> ServiceResponse {
|
||||
let app = test_app(
|
||||
test_app_state(TestAppStateParams {
|
||||
schedule: if data.load_schedule {
|
||||
TestScheduleType::Local
|
||||
} else {
|
||||
TestScheduleType::None
|
||||
},
|
||||
})
|
||||
.await,
|
||||
sign_up,
|
||||
)
|
||||
.await;
|
||||
async fn sign_up_client(data: SignUpPartial<'_>) -> ServiceResponse {
|
||||
let app = test_app(test_app_state().await, sign_up).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-up")
|
||||
.method(Method::POST)
|
||||
.set_json(Request {
|
||||
username: data.username.clone(),
|
||||
username: data.username.to_string(),
|
||||
password: "example".to_string(),
|
||||
group: data.group.clone(),
|
||||
group: data.group.to_string(),
|
||||
role: data.role.clone(),
|
||||
version: "1.0.0".to_string(),
|
||||
})
|
||||
@@ -292,15 +281,14 @@ mod tests {
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state().await;
|
||||
driver::users::delete_by_username(&app_state, &"test::sign_up_valid".to_string());
|
||||
driver::users::delete_by_username(&app_state, &"test::sign_up_valid".to_string()).await;
|
||||
|
||||
// test
|
||||
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_valid".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
username: "test::sign_up_valid",
|
||||
group: "ИС-214/23",
|
||||
role: UserRole::Student,
|
||||
load_schedule: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -314,23 +302,21 @@ mod tests {
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state().await;
|
||||
driver::users::delete_by_username(&app_state, &"test::sign_up_multiple".to_string());
|
||||
driver::users::delete_by_username(&app_state, &"test::sign_up_multiple".to_string()).await;
|
||||
|
||||
let create = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_multiple".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
username: "test::sign_up_multiple",
|
||||
group: "ИС-214/23",
|
||||
role: UserRole::Student,
|
||||
load_schedule: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(create.status(), StatusCode::OK);
|
||||
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_multiple".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
username: "test::sign_up_multiple",
|
||||
group: "ИС-214/23",
|
||||
role: UserRole::Student,
|
||||
load_schedule: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -343,10 +329,9 @@ mod tests {
|
||||
|
||||
// test
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_invalid_role".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
username: "test::sign_up_invalid_role",
|
||||
group: "ИС-214/23",
|
||||
role: UserRole::Admin,
|
||||
load_schedule: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -359,10 +344,9 @@ mod tests {
|
||||
|
||||
// test
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_invalid_group".to_string(),
|
||||
group: "invalid_group".to_string(),
|
||||
username: "test::sign_up_invalid_group",
|
||||
group: "invalid_group",
|
||||
role: UserRole::Student,
|
||||
load_schedule: true,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod update_callback;
|
||||
mod set_token;
|
||||
mod update_callback;
|
||||
|
||||
pub use update_callback::*;
|
||||
pub use set_token::*;
|
||||
pub use update_callback::*;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::database;
|
||||
use crate::database::models::FCM;
|
||||
use crate::extractors::authorized_user::UserExtractor;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::utility::mutex::{MutexScope, MutexScopeAsync};
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{HttpResponse, Responder, patch, web};
|
||||
use diesel::{RunQueryDsl, SaveChangesDsl};
|
||||
use firebase_messaging_rs::FCMClient;
|
||||
use firebase_messaging_rs::topic::{TopicManagementError, TopicManagementSupport};
|
||||
use firebase_messaging_rs::topic::TopicManagementSupport;
|
||||
use serde::Deserialize;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Params {
|
||||
@@ -34,11 +33,10 @@ async fn get_fcm(
|
||||
topics: vec![],
|
||||
};
|
||||
|
||||
match app_state.database.scope(|conn| {
|
||||
diesel::insert_into(database::schema::fcm::table)
|
||||
.values(&fcm)
|
||||
.execute(conn)
|
||||
}) {
|
||||
match diesel::insert_into(database::schema::fcm::table)
|
||||
.values(&fcm)
|
||||
.execute(app_state.get_database().await.deref_mut())
|
||||
{
|
||||
Ok(_) => Ok(fcm),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
@@ -51,7 +49,7 @@ async fn get_fcm(
|
||||
pub async fn set_token(
|
||||
app_state: web::Data<AppState>,
|
||||
web::Query(params): web::Query<Params>,
|
||||
user_data: SyncExtractor<UserExtractor<true>>,
|
||||
user_data: AsyncExtractor<UserExtractor<true>>,
|
||||
) -> impl Responder {
|
||||
let user_data = user_data.into_inner();
|
||||
|
||||
@@ -75,39 +73,21 @@ pub async fn set_token(
|
||||
fcm.topics.push(Some("common".to_string()));
|
||||
}
|
||||
|
||||
// Subscribe to default topics.
|
||||
if let Some(e) = app_state
|
||||
.fcm_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.async_scope(
|
||||
async |client: &mut FCMClient| -> Result<(), TopicManagementError> {
|
||||
let mut tokens: Vec<String> = Vec::new();
|
||||
tokens.push(fcm.token.clone());
|
||||
fcm.save_changes::<FCM>(app_state.get_database().await.deref_mut())
|
||||
.unwrap();
|
||||
|
||||
for topic in fcm.topics.clone() {
|
||||
if let Some(topic) = topic {
|
||||
client.register_tokens_to_topic(topic.clone(), tokens.clone()).await?;
|
||||
}
|
||||
}
|
||||
let fcm_client = app_state.get_fcm_client().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
{
|
||||
eprintln!("Failed to subscribe token to topic: {:?}", e);
|
||||
return HttpResponse::Ok();
|
||||
}
|
||||
|
||||
// Write updates to db.
|
||||
if let Some(e) = app_state
|
||||
.database
|
||||
.scope(|conn| fcm.save_changes::<FCM>(conn))
|
||||
.err()
|
||||
{
|
||||
eprintln!("Failed to update FCM object: {e}");
|
||||
for topic in fcm.topics.clone() {
|
||||
if let Some(topic) = topic {
|
||||
if let Err(error) = fcm_client
|
||||
.register_token_to_topic(&*topic, &*fcm.token)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to subscribe token to topic: {:?}", error);
|
||||
return HttpResponse::Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::database::driver::users::UserSave;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::utility::mutex::MutexScope;
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{HttpResponse, Responder, post, web};
|
||||
use diesel::SaveChangesDsl;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK),
|
||||
@@ -13,20 +12,13 @@ use diesel::SaveChangesDsl;
|
||||
async fn update_callback(
|
||||
app_state: web::Data<AppState>,
|
||||
version: web::Path<String>,
|
||||
user: SyncExtractor<User>,
|
||||
user: AsyncExtractor<User>,
|
||||
) -> impl Responder {
|
||||
let mut user = user.into_inner();
|
||||
|
||||
user.version = version.into_inner();
|
||||
user.android_version = Some(version.into_inner());
|
||||
|
||||
match app_state
|
||||
.database
|
||||
.scope(|conn| user.save_changes::<User>(conn))
|
||||
{
|
||||
Ok(_) => HttpResponse::Ok(),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to update user: {}", e);
|
||||
HttpResponse::InternalServerError()
|
||||
}
|
||||
}
|
||||
user.save(&app_state).await.unwrap();
|
||||
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
5
src/routes/flow/mod.rs
Normal file
5
src/routes/flow/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod telegram_auth;
|
||||
mod telegram_complete;
|
||||
|
||||
pub use telegram_auth::*;
|
||||
pub use telegram_complete::*;
|
||||
183
src/routes/flow/telegram_auth.rs
Normal file
183
src/routes/flow/telegram_auth.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use self::schema::*;
|
||||
use crate::database::driver;
|
||||
use crate::database::driver::users::UserSave;
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::schema::ResponseError;
|
||||
use crate::utility::telegram::{WebAppInitDataMap, WebAppUser};
|
||||
use crate::{AppState, utility};
|
||||
use actix_web::{post, web};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use objectid::ObjectId;
|
||||
use std::sync::Arc;
|
||||
use web::Json;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(status = UNAUTHORIZED, body = ResponseError<ErrorCode>),
|
||||
))]
|
||||
#[post("/telegram-auth")]
|
||||
pub async fn telegram_auth(
|
||||
data_json: Json<Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> ServiceResponse {
|
||||
let init_data = WebAppInitDataMap::from_str(data_json.into_inner().init_data);
|
||||
|
||||
// for (key, value) in &init_data.data_map {
|
||||
// println!("key: {} | value: {}", key, value);
|
||||
// }
|
||||
|
||||
{
|
||||
let env = &app_state.get_env().telegram;
|
||||
|
||||
if let Err(error) = init_data.verify(env.bot_id, env.test_dc) {
|
||||
return Err(ErrorCode::InvalidInitData(Arc::new(error))).into();
|
||||
}
|
||||
}
|
||||
|
||||
let auth_date = DateTime::<Utc>::from_timestamp(
|
||||
init_data
|
||||
.data_map
|
||||
.get("auth_date")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if Utc::now() - auth_date > Duration::minutes(5) {
|
||||
return Err(ErrorCode::ExpiredInitData).into();
|
||||
}
|
||||
|
||||
let web_app_user =
|
||||
serde_json::from_str::<WebAppUser>(init_data.data_map.get("user").unwrap()).unwrap();
|
||||
|
||||
let mut user = {
|
||||
match driver::users::get_by_telegram_id(&app_state, web_app_user.id).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => {
|
||||
let new_user = User {
|
||||
id: ObjectId::new().unwrap().to_string(),
|
||||
username: format!("telegram_{}", web_app_user.id), // можно оставить, а можно поменять
|
||||
password: None, // ибо нехуй
|
||||
vk_id: None,
|
||||
telegram_id: Some(web_app_user.id),
|
||||
access_token: None, // установится ниже
|
||||
group: None,
|
||||
role: UserRole::Student, // TODO: при реге проверять данные
|
||||
android_version: None,
|
||||
};
|
||||
|
||||
driver::users::insert(&app_state, &new_user)
|
||||
.await
|
||||
.map(|_| new_user)
|
||||
}
|
||||
}
|
||||
.expect("Failed to get or add user")
|
||||
};
|
||||
|
||||
user.access_token = Some(utility::jwt::encode(&user.id));
|
||||
|
||||
user.save(&app_state).await.expect("Failed to update user");
|
||||
|
||||
Ok(Response::new(
|
||||
&*user.access_token.unwrap(),
|
||||
user.group.is_some(),
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schema::PartialOkResponse;
|
||||
use crate::state::AppState;
|
||||
use crate::utility::telegram::VerifyError;
|
||||
use actix_macros::ErrResponse;
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::cookie::CookieBuilder;
|
||||
use actix_web::cookie::time::OffsetDateTime;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(as = Flow::TelegramAuth::Request)]
|
||||
pub struct Request {
|
||||
/// Telegram WebApp init data.
|
||||
pub init_data: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(as = Flow::TelegramAuth::Response)]
|
||||
pub struct Response {
|
||||
#[serde(skip)]
|
||||
#[schema(ignore)]
|
||||
access_token: String,
|
||||
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(access_token: &str, completed: bool) -> Self {
|
||||
Self {
|
||||
access_token: access_token.to_string(),
|
||||
completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOkResponse for Response {
|
||||
fn post_process(
|
||||
&mut self,
|
||||
request: &HttpRequest,
|
||||
response: &mut HttpResponse<EitherBody<String>>,
|
||||
) -> () {
|
||||
let access_token = &self.access_token;
|
||||
|
||||
let app_state = request.app_data::<web::Data<AppState>>().unwrap();
|
||||
let mini_app_host = &*app_state.get_env().telegram.mini_app_host;
|
||||
|
||||
let cookie = CookieBuilder::new("access_token", access_token)
|
||||
.domain(mini_app_host)
|
||||
.path("/")
|
||||
.expires(
|
||||
OffsetDateTime::now_utc().add(std::time::Duration::from_secs(60 * 60 * 24 * 7)),
|
||||
)
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
|
||||
response.add_cookie(&cookie).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
|
||||
#[derive(Clone, ToSchema, Display, ErrResponse)]
|
||||
#[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"]
|
||||
#[schema(as = Flow::TelegramAuth::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
#[display("Invalid init data provided: {_0}")]
|
||||
#[schema(value_type = String)]
|
||||
InvalidInitData(Arc<VerifyError>),
|
||||
|
||||
#[display("Expired init data provided.")]
|
||||
ExpiredInitData,
|
||||
}
|
||||
|
||||
impl Serialize for ErrorCode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ErrorCode::InvalidInitData(_) => serializer.serialize_str("INVALID_INIT_DATA"),
|
||||
ErrorCode::ExpiredInitData => serializer.serialize_str("EXPIRED_INIT_DATA"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/routes/flow/telegram_complete.rs
Normal file
94
src/routes/flow/telegram_complete.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::database::driver;
|
||||
use crate::database::driver::users::UserSave;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use actix_web::{post, web};
|
||||
use web::Json;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK),
|
||||
(status = CONFLICT, body = ResponseError<ErrorCode>),
|
||||
(status = INTERNAL_SERVER_ERROR, body = ResponseError<ErrorCode>),
|
||||
(status = BAD_REQUEST, body = ResponseError<ErrorCode>)
|
||||
))]
|
||||
#[post("/telegram-complete")]
|
||||
pub async fn telegram_complete(
|
||||
data: Json<Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
user: AsyncExtractor<User>,
|
||||
) -> ServiceResponse {
|
||||
let mut user = user.into_inner();
|
||||
|
||||
// проверка на перезапись уже имеющихся данных
|
||||
if user.group.is_some() {
|
||||
return Err(ErrorCode::AlreadyCompleted).into();
|
||||
}
|
||||
|
||||
let data = data.into_inner();
|
||||
|
||||
// замена существующего имени, если оно отличается
|
||||
if user.username != data.username {
|
||||
if driver::users::contains_by_username(&app_state, &data.username).await {
|
||||
return Err(ErrorCode::UsernameAlreadyExists).into();
|
||||
}
|
||||
|
||||
user.username = data.username;
|
||||
}
|
||||
|
||||
// проверка на существование группы
|
||||
if !app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.contains_key(&data.group)
|
||||
{
|
||||
return Err(ErrorCode::InvalidGroupName).into();
|
||||
}
|
||||
|
||||
user.group = Some(data.group);
|
||||
|
||||
user.save(&app_state).await.expect("Failed to update user");
|
||||
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||
#[schema(as = Flow::TelegramFill::Request)]
|
||||
pub struct Request {
|
||||
/// Username.
|
||||
pub username: String,
|
||||
|
||||
/// Group.
|
||||
pub group: String,
|
||||
}
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<(), ErrorCode>;
|
||||
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = Flow::TelegramFill::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
#[display("This flow already completed.")]
|
||||
#[status_code = "actix_web::http::StatusCode::CONFLICT"]
|
||||
AlreadyCompleted,
|
||||
|
||||
#[display("Username is already exists.")]
|
||||
#[status_code = "actix_web::http::StatusCode::BAD_REQUEST"]
|
||||
UsernameAlreadyExists,
|
||||
|
||||
#[display("The required group does not exist.")]
|
||||
#[status_code = "actix_web::http::StatusCode::BAD_REQUEST"]
|
||||
InvalidGroupName,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod auth;
|
||||
pub mod fcm;
|
||||
pub mod flow;
|
||||
pub mod schedule;
|
||||
mod schema;
|
||||
pub mod users;
|
||||
|
||||
@@ -7,17 +7,5 @@ use actix_web::{get, web};
|
||||
))]
|
||||
#[get("/cache-status")]
|
||||
pub async fn 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()
|
||||
CacheStatus::from(&app_state).await.into()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(status = OK, body = ScheduleEntryResponse),
|
||||
(
|
||||
status = SERVICE_UNAVAILABLE,
|
||||
body = ResponseError<ErrorCode>,
|
||||
@@ -25,68 +26,42 @@ use actix_web::{get, web};
|
||||
),
|
||||
))]
|
||||
#[get("/group")]
|
||||
pub async fn group(user: SyncExtractor<User>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn group(user: AsyncExtractor<User>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match &user.into_inner().group {
|
||||
None => Err(ErrorCode::SignUpNotCompleted),
|
||||
|
||||
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(),
|
||||
Some(group) => match app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.get(group)
|
||||
{
|
||||
None => Err(ErrorCode::NotFound),
|
||||
|
||||
Some(entry) => Ok(entry.clone().into()),
|
||||
},
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleEntryResponse, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[schema(as = GetGroup::Response)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
/// Group schedule.
|
||||
pub group: ScheduleEntry,
|
||||
|
||||
/// ## Outdated variable.
|
||||
///
|
||||
/// By default, an empty list is returned.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated: Vec<i32>,
|
||||
|
||||
/// ## Outdated variable.
|
||||
///
|
||||
/// By default, the initial date for 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)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = GroupSchedule::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules have not yet been parsed.
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
/// The user tried to access the API without completing singing up.
|
||||
#[status_code = "actix_web::http::StatusCode::FORBIDDEN"]
|
||||
#[display("You have not completed signing up.")]
|
||||
SignUpNotCompleted,
|
||||
|
||||
/// Group not found.
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
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>),
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = Response)))]
|
||||
#[get("/group-names")]
|
||||
pub async fn group_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn group_names(app_state: web::Data<AppState>) -> Response {
|
||||
let mut names: Vec<String> = app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
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()
|
||||
names.sort();
|
||||
|
||||
Response { names }
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use actix_macros::ResponderJson;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, ResponderJson)]
|
||||
#[schema(as = GetGroupNames::Response)]
|
||||
pub struct Response {
|
||||
/// List of group names sorted in alphabetical order.
|
||||
#[schema(examples(json!(["ИС-214/23"])))]
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Response {
|
||||
fn from(names: Vec<String>) -> Self {
|
||||
Self { names }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ mod schedule;
|
||||
mod schema;
|
||||
mod teacher;
|
||||
mod teacher_names;
|
||||
mod update_download_url;
|
||||
|
||||
pub use cache_status::*;
|
||||
pub use group::*;
|
||||
@@ -13,4 +12,3 @@ pub use group_names::*;
|
||||
pub use schedule::*;
|
||||
pub use teacher::*;
|
||||
pub use teacher_names::*;
|
||||
pub use update_download_url::*;
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
use self::schema::*;
|
||||
use crate::app_state::AppState;
|
||||
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::routes::schedule::schema::ScheduleView;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = ScheduleView),
|
||||
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>)
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = ScheduleView)))]
|
||||
#[get("/")]
|
||||
pub async fn schedule(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match ScheduleView::try_from(&app_state) {
|
||||
Ok(res) => Ok(res).into(),
|
||||
Err(e) => match e {
|
||||
ErrorCode::NoSchedule => ErrorCode::NoSchedule.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleView, ErrorCode>;
|
||||
pub async fn schedule(app_state: web::Data<AppState>) -> ScheduleView {
|
||||
ScheduleView::from(&app_state).await
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
use crate::app_state::{AppState, Schedule};
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, ResponderJson, StatusCode};
|
||||
use crate::state::{AppState, ScheduleSnapshot};
|
||||
use actix_macros::{OkResponse, ResponderJson};
|
||||
use actix_web::web;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use derive_more::Display;
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response from schedule server.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, OkResponse, ResponderJson)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScheduleView {
|
||||
/// ETag schedules on polytechnic server.
|
||||
etag: String,
|
||||
|
||||
/// Schedule update date on polytechnic website.
|
||||
uploaded_at: DateTime<Utc>,
|
||||
|
||||
/// Date last downloaded from the Polytechnic server.
|
||||
downloaded_at: DateTime<Utc>,
|
||||
/// Url to xls file.
|
||||
url: String,
|
||||
|
||||
/// Groups schedule.
|
||||
groups: HashMap<String, ScheduleEntry>,
|
||||
@@ -28,80 +21,55 @@ pub struct ScheduleView {
|
||||
teachers: HashMap<String, ScheduleEntry>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Schedules not yet parsed.
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
#[derive(Serialize, ToSchema, OkResponse)]
|
||||
pub struct ScheduleEntryResponse(ScheduleEntry);
|
||||
|
||||
impl From<ScheduleEntry> for ScheduleEntryResponse {
|
||||
fn from(value: ScheduleEntry) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&web::Data<AppState>> for ScheduleView {
|
||||
type Error = ErrorCode;
|
||||
impl ScheduleView {
|
||||
pub async fn from(app_state: &web::Data<AppState>) -> Self {
|
||||
let schedule = app_state.get_schedule_snapshot().await.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 {
|
||||
etag: schedule.etag,
|
||||
uploaded_at: schedule.updated_at,
|
||||
downloaded_at: schedule.parsed_at,
|
||||
groups: schedule.data.groups,
|
||||
teachers: schedule.data.teachers,
|
||||
})
|
||||
} else {
|
||||
Err(ErrorCode::NoSchedule)
|
||||
Self {
|
||||
url: schedule.url,
|
||||
groups: schedule.data.groups,
|
||||
teachers: schedule.data.teachers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached schedule status.
|
||||
#[derive(Serialize, Deserialize, ToSchema, ResponderJson)]
|
||||
#[derive(Serialize, Deserialize, ToSchema, ResponderJson, OkResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CacheStatus {
|
||||
/// Schedule hash.
|
||||
pub cache_hash: String,
|
||||
|
||||
/// Whether the schedule reference needs to be updated.
|
||||
pub cache_update_required: bool,
|
||||
pub hash: String,
|
||||
|
||||
/// Last cache update date.
|
||||
pub last_cache_update: i64,
|
||||
pub fetched_at: i64,
|
||||
|
||||
/// Cached schedule update date.
|
||||
///
|
||||
/// Determined by the polytechnic's server.
|
||||
pub last_schedule_update: i64,
|
||||
pub updated_at: 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,
|
||||
}
|
||||
pub async fn from(value: &web::Data<AppState>) -> Self {
|
||||
From::<&ScheduleSnapshot>::from(value.get_schedule_snapshot().await.deref())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
impl From<&ScheduleSnapshot> for CacheStatus {
|
||||
fn from(value: &ScheduleSnapshot) -> Self {
|
||||
Self {
|
||||
cache_hash: value.hash(),
|
||||
cache_update_required: (Utc::now() - value.fetched_at) > Duration::minutes(5),
|
||||
last_cache_update: value.fetched_at.timestamp(),
|
||||
last_schedule_update: value.updated_at.timestamp(),
|
||||
hash: value.hash(),
|
||||
fetched_at: value.fetched_at.timestamp(),
|
||||
updated_at: value.updated_at.timestamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
use self::schema::*;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::AppState;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use actix_web::{get, web};
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(
|
||||
status = SERVICE_UNAVAILABLE,
|
||||
body = ResponseError<ErrorCode>,
|
||||
example = json!({
|
||||
"code": "NO_SCHEDULE",
|
||||
"message": "Schedule not parsed yet."
|
||||
})
|
||||
),
|
||||
(status = OK, body = ScheduleEntry),
|
||||
(
|
||||
status = NOT_FOUND,
|
||||
body = ResponseError<ErrorCode>,
|
||||
@@ -23,72 +16,34 @@ use actix_web::{get, web};
|
||||
),
|
||||
))]
|
||||
#[get("/teacher/{name}")]
|
||||
pub async fn teacher(
|
||||
name: web::Path<String>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn teacher(name: web::Path<String>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.teachers
|
||||
.get(&name.into_inner())
|
||||
{
|
||||
None => Err(ErrorCode::NotFound),
|
||||
|
||||
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(),
|
||||
},
|
||||
Some(entry) => Ok(entry.clone().into()),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleEntryResponse, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[schema(as = GetTeacher::Response)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
/// Teacher's schedule.
|
||||
pub teacher: ScheduleEntry,
|
||||
|
||||
/// ## Deprecated variable.
|
||||
///
|
||||
/// By default, an empty list is returned.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated: Vec<i32>,
|
||||
|
||||
/// ## Deprecated variable.
|
||||
///
|
||||
/// Defaults to the Unix start date.
|
||||
#[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)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = TeacherSchedule::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules have not yet been parsed.
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
|
||||
/// Teacher not found.
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||
#[display("Required teacher not found.")]
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
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>),
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = Response)))]
|
||||
#[get("/teacher-names")]
|
||||
pub async fn teacher_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn teacher_names(app_state: web::Data<AppState>) -> Response {
|
||||
let mut names: Vec<String> = app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.teachers
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
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();
|
||||
names.sort();
|
||||
|
||||
Ok(names.into()).into()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
Response { names }
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use actix_macros::ResponderJson;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, ResponderJson)]
|
||||
#[schema(as = GetTeacherNames::Response)]
|
||||
pub struct Response {
|
||||
/// List of teacher names sorted alphabetically.
|
||||
#[schema(examples(json!(["Хомченко Н.Е."])))]
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Response {
|
||||
fn from(names: Vec<String>) -> Self {
|
||||
Self { names }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::app_state::Schedule;
|
||||
use schedule_parser::parse_xls;
|
||||
use crate::routes::schedule::schema::CacheStatus;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::xls_downloader::interface::{FetchError, 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.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) => {
|
||||
sentry::capture_error(&error);
|
||||
|
||||
ErrorCode::InvalidSchedule(error).into_response()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
if let FetchError::Unknown(error) = &error {
|
||||
sentry::capture_error(&error);
|
||||
}
|
||||
|
||||
ErrorCode::DownloadFailed(error).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if let FetchError::Unknown(error) = &error {
|
||||
sentry::capture_error(&error);
|
||||
}
|
||||
|
||||
ErrorCode::FetchFailed(error).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_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;
|
||||
use crate::xls_downloader::interface::FetchError;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<CacheStatus, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Request {
|
||||
/// Schedule link.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||
#[schema(as = SetDownloadUrl::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Transferred link with host different from politehnikum-eng.ru.
|
||||
#[display("URL with unknown host provided. Provide url with 'politehnikum-eng.ru' host.")]
|
||||
NonWhitelistedHost,
|
||||
|
||||
/// Failed to retrieve file metadata.
|
||||
#[display("Unable to retrieve metadata from the specified URL: {_0}")]
|
||||
FetchFailed(FetchError),
|
||||
|
||||
/// Failed to download the file.
|
||||
#[display("Unable to retrieve data from the specified URL: {_0}")]
|
||||
DownloadFailed(FetchError),
|
||||
|
||||
/// The link leads to an outdated schedule.
|
||||
///
|
||||
/// An outdated schedule refers to a schedule that was published earlier
|
||||
/// than is currently available.
|
||||
#[display("The schedule is older than it already is.")]
|
||||
OutdatedSchedule,
|
||||
|
||||
/// Failed to parse the schedule.
|
||||
#[display("{_0}")]
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,19 @@ use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::convert::Into;
|
||||
use std::fmt::Display;
|
||||
use utoipa::PartialSchema;
|
||||
|
||||
pub struct Response<T, E>(pub Result<T, E>)
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
E: Serialize + PartialSchema + Clone + PartialStatusCode;
|
||||
|
||||
pub trait PartialStatusCode {
|
||||
fn status_code(&self) -> StatusCode;
|
||||
}
|
||||
T: Serialize + PartialSchema + PartialOkResponse,
|
||||
E: Serialize + PartialSchema + Display + PartialErrResponse;
|
||||
|
||||
/// Transform Response<T, E> into Result<T, E>
|
||||
impl<T, E> Into<Result<T, E>> for Response<T, E>
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
E: Serialize + PartialSchema + Clone + PartialStatusCode,
|
||||
T: Serialize + PartialSchema + PartialOkResponse,
|
||||
E: Serialize + PartialSchema + Display + PartialErrResponse,
|
||||
{
|
||||
fn into(self) -> Result<T, E> {
|
||||
self.0
|
||||
@@ -29,8 +26,8 @@ where
|
||||
/// Transform T into Response<T, E>
|
||||
impl<T, E> From<Result<T, E>> for Response<T, E>
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
E: Serialize + PartialSchema + Clone + PartialStatusCode,
|
||||
T: Serialize + PartialSchema + PartialOkResponse,
|
||||
E: Serialize + PartialSchema + Display + PartialErrResponse,
|
||||
{
|
||||
fn from(value: Result<T, E>) -> Self {
|
||||
Response(value)
|
||||
@@ -40,17 +37,16 @@ where
|
||||
/// Serialize Response<T, E>
|
||||
impl<T, E> Serialize for Response<T, E>
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
E: Serialize + PartialSchema + Clone + PartialStatusCode + Into<ResponseError<E>>,
|
||||
T: Serialize + PartialSchema + PartialOkResponse,
|
||||
E: Serialize + PartialSchema + Display + PartialErrResponse + Clone + Into<ResponseError<E>>,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match &self.0 {
|
||||
Ok(ok) => serializer.serialize_some::<T>(&ok),
|
||||
Err(err) => serializer
|
||||
.serialize_some::<ResponseError<E>>(&ResponseError::<E>::from(err.clone().into())),
|
||||
Ok(ok) => serializer.serialize_some(&ok),
|
||||
Err(err) => serializer.serialize_some(&ResponseError::<E>::from(err.clone().into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,12 +54,12 @@ where
|
||||
/// Transform Response<T, E> to HttpResponse<String>
|
||||
impl<T, E> Responder for Response<T, E>
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
E: Serialize + PartialSchema + Clone + PartialStatusCode + Into<ResponseError<E>>,
|
||||
T: Serialize + PartialSchema + PartialOkResponse,
|
||||
E: Serialize + PartialSchema + Display + PartialErrResponse + Clone + Into<ResponseError<E>>,
|
||||
{
|
||||
type Body = EitherBody<String>;
|
||||
|
||||
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
fn respond_to(mut self, request: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match serde_json::to_string(&self) {
|
||||
Ok(body) => {
|
||||
let code = match &self.0 {
|
||||
@@ -71,13 +67,19 @@ where
|
||||
Err(e) => e.status_code(),
|
||||
};
|
||||
|
||||
match HttpResponse::build(code)
|
||||
let mut response = match HttpResponse::build(code)
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.message_body(body)
|
||||
{
|
||||
Ok(res) => res.map_into_left_body(),
|
||||
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
|
||||
};
|
||||
|
||||
if let Ok(ok) = &mut self.0 {
|
||||
ok.post_process(request, &mut response);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
@@ -87,61 +89,80 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// ResponseError<T>
|
||||
///
|
||||
/// Field `message` is optional for backwards compatibility with Android App, that produces error if new fields will be added to JSON response.
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct ResponseError<T: Serialize + PartialSchema> {
|
||||
pub code: T,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
/// Трейт для всех положительных ответов от сервера
|
||||
pub trait PartialOkResponse {
|
||||
fn post_process(
|
||||
&mut self,
|
||||
_request: &HttpRequest,
|
||||
_response: &mut HttpResponse<EitherBody<String>>,
|
||||
) -> () {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoResponseAsError<T>
|
||||
impl PartialOkResponse for () {}
|
||||
|
||||
/// Трейт для всех отрицательных ответов от сервера
|
||||
pub trait PartialErrResponse {
|
||||
fn status_code(&self) -> StatusCode;
|
||||
}
|
||||
|
||||
/// ResponseError<T>
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct ResponseError<T: Serialize + PartialSchema + Clone> {
|
||||
pub code: T,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl<T> From<T> for ResponseError<T>
|
||||
where
|
||||
T: Serialize + PartialSchema,
|
||||
Self: Serialize + PartialSchema + Clone + PartialStatusCode + Into<ResponseError<Self>>,
|
||||
T: Serialize + PartialSchema + Display + Clone,
|
||||
{
|
||||
fn into_response(self) -> Response<T, Self> {
|
||||
Response(Err(self))
|
||||
fn from(code: T) -> Self {
|
||||
Self {
|
||||
message: format!("{}", code),
|
||||
code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod user {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use actix_macros::ResponderJson;
|
||||
use actix_macros::{OkResponse, ResponderJson};
|
||||
use serde::Serialize;
|
||||
|
||||
//noinspection SpellCheckingInspection
|
||||
/// Используется для скрытия чувствительных полей, таких как хеш пароля или FCM
|
||||
#[derive(Serialize, utoipa::ToSchema, ResponderJson)]
|
||||
#[derive(Serialize, utoipa::ToSchema, ResponderJson, OkResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserResponse {
|
||||
/// UUID
|
||||
#[schema(examples("67dcc9a9507b0000772744a2"))]
|
||||
id: String,
|
||||
pub id: String,
|
||||
|
||||
/// Имя пользователя
|
||||
#[schema(examples("n08i40k"))]
|
||||
username: String,
|
||||
pub username: String,
|
||||
|
||||
/// Группа
|
||||
#[schema(examples("ИС-214/23"))]
|
||||
group: String,
|
||||
pub group: Option<String>,
|
||||
|
||||
/// Роль
|
||||
role: UserRole,
|
||||
pub role: UserRole,
|
||||
|
||||
/// Идентификатор привязанного аккаунта VK
|
||||
#[schema(examples(498094647, json!(null)))]
|
||||
vk_id: Option<i32>,
|
||||
pub vk_id: Option<i32>,
|
||||
|
||||
/// Идентификатор привязанного аккаунта Telegram
|
||||
#[schema(examples(996004735, json!(null)))]
|
||||
pub telegram_id: Option<i64>,
|
||||
|
||||
/// JWT токен доступа
|
||||
#[schema(examples(
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY"
|
||||
))]
|
||||
access_token: String,
|
||||
pub access_token: Option<String>,
|
||||
}
|
||||
|
||||
/// Create UserResponse from User ref.
|
||||
@@ -153,6 +174,7 @@ pub mod user {
|
||||
group: user.group.clone(),
|
||||
role: user.role.clone(),
|
||||
vk_id: user.vk_id.clone(),
|
||||
telegram_id: user.telegram_id.clone(),
|
||||
access_token: user.access_token.clone(),
|
||||
}
|
||||
}
|
||||
@@ -167,6 +189,7 @@ pub mod user {
|
||||
group: user.group,
|
||||
role: user.role,
|
||||
vk_id: user.vk_id,
|
||||
telegram_id: user.telegram_id,
|
||||
access_token: user.access_token,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,62 @@
|
||||
use self::schema::*;
|
||||
use crate::app_state::AppState;
|
||||
use crate::database::driver::users::UserSave;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::routes::schema::IntoResponseAsError;
|
||||
use crate::utility::mutex::MutexScope;
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{post, web};
|
||||
|
||||
#[utoipa::path(responses((status = OK)))]
|
||||
#[post("/change-group")]
|
||||
pub async fn change_group(
|
||||
app_state: web::Data<AppState>,
|
||||
user: SyncExtractor<User>,
|
||||
user: AsyncExtractor<User>,
|
||||
data: web::Json<Request>,
|
||||
) -> ServiceResponse {
|
||||
let mut user = user.into_inner();
|
||||
|
||||
if user.group == data.group {
|
||||
return ErrorCode::SameGroup.into_response();
|
||||
if user.group.is_some_and(|group| group == data.group) {
|
||||
return Ok(()).into();
|
||||
}
|
||||
|
||||
if let Some(e) = app_state.schedule.scope(|schedule| match schedule {
|
||||
Some(schedule) => {
|
||||
if schedule.data.groups.contains_key(&data.group) {
|
||||
None
|
||||
} else {
|
||||
Some(ErrorCode::NotFound)
|
||||
}
|
||||
}
|
||||
None => Some(ErrorCode::NoSchedule),
|
||||
}) {
|
||||
return e.into_response();
|
||||
if !app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.contains_key(&data.group)
|
||||
{
|
||||
return Err(ErrorCode::NotFound).into();
|
||||
}
|
||||
|
||||
user.group = data.into_inner().group;
|
||||
|
||||
if let Some(e) = user.save(&app_state).err() {
|
||||
eprintln!("Failed to update user: {e}");
|
||||
return ErrorCode::InternalServerError.into_response();
|
||||
}
|
||||
|
||||
|
||||
user.group = Some(data.into_inner().group);
|
||||
user.save(&app_state).await.unwrap();
|
||||
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<(), ErrorCode>;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
#[schema(as = ChangeGroup::Request)]
|
||||
pub struct Request {
|
||||
/// Group name.
|
||||
// Group.
|
||||
pub group: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = ChangeGroup::ErrorCode)]
|
||||
#[status_code = "actix_web::http::StatusCode::CONFLICT"]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules have not yet been received.
|
||||
#[display("Schedule not parsed yet.")]
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
NoSchedule,
|
||||
|
||||
/// Passed the same group name that is currently there.
|
||||
#[display("Passed the same group name as it is at the moment.")]
|
||||
SameGroup,
|
||||
|
||||
/// The required group does not exist.
|
||||
#[display("The required group does not exist.")]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||
NotFound,
|
||||
|
||||
/// Server-side error.
|
||||
#[display("Internal server error.")]
|
||||
#[status_code = "actix_web::http::StatusCode::INTERNAL_SERVER_ERROR"]
|
||||
InternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
use self::schema::*;
|
||||
use crate::app_state::AppState;
|
||||
use crate::database::driver;
|
||||
use crate::database::driver::users::UserSave;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::routes::schema::IntoResponseAsError;
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{post, web};
|
||||
|
||||
#[utoipa::path(responses((status = OK)))]
|
||||
#[post("/change-username")]
|
||||
pub async fn change_username(
|
||||
app_state: web::Data<AppState>,
|
||||
user: SyncExtractor<User>,
|
||||
user: AsyncExtractor<User>,
|
||||
data: web::Json<Request>,
|
||||
) -> ServiceResponse {
|
||||
let mut user = user.into_inner();
|
||||
|
||||
if user.username == data.username {
|
||||
return ErrorCode::SameUsername.into_response();
|
||||
return Ok(()).into();
|
||||
}
|
||||
|
||||
if driver::users::get_by_username(&app_state, &data.username).is_ok() {
|
||||
return ErrorCode::AlreadyExists.into_response();
|
||||
if driver::users::get_by_username(&app_state, &data.username)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(ErrorCode::AlreadyExists).into();
|
||||
}
|
||||
|
||||
user.username = data.into_inner().username;
|
||||
|
||||
if let Some(e) = user.save(&app_state).err() {
|
||||
eprintln!("Failed to update user: {e}");
|
||||
return ErrorCode::InternalServerError.into_response();
|
||||
}
|
||||
user.save(&app_state).await.unwrap();
|
||||
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -49,22 +47,13 @@ mod schema {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = ChangeUsername::ErrorCode)]
|
||||
#[status_code = "actix_web::http::StatusCode::CONFLICT"]
|
||||
pub enum ErrorCode {
|
||||
/// The same name that is currently present is passed.
|
||||
#[display("Passed the same name as it is at the moment.")]
|
||||
SameUsername,
|
||||
|
||||
/// A user with this name already exists.
|
||||
#[display("A user with this name already exists.")]
|
||||
AlreadyExists,
|
||||
|
||||
/// Server-side error.
|
||||
#[display("Internal server error.")]
|
||||
#[status_code = "actix_web::http::StatusCode::INTERNAL_SERVER_ERROR"]
|
||||
InternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use actix_web::get;
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::routes::schema::user::UserResponse;
|
||||
use actix_web::get;
|
||||
|
||||
#[utoipa::path(responses((status = OK, body = UserResponse)))]
|
||||
#[get("/me")]
|
||||
pub async fn me(user: SyncExtractor<User>) -> UserResponse {
|
||||
pub async fn me(user: AsyncExtractor<User>) -> UserResponse {
|
||||
user.into_inner().into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use self::schema::*;
|
||||
use crate::app_state::AppState;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::routes::schema::ResponseError;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{post, web};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -35,7 +35,7 @@ async fn oauth(data: web::Json<Request>, app_state: web::Data<AppState>) -> Serv
|
||||
let data = data.into_inner();
|
||||
let state = Uuid::new_v4().simple().to_string();
|
||||
|
||||
let vk_id = &app_state.vk_id;
|
||||
let vk_id = &app_state.get_env().vk_id;
|
||||
let client_id = vk_id.client_id.clone().to_string();
|
||||
|
||||
let mut params = HashMap::new();
|
||||
@@ -56,27 +56,27 @@ async fn oauth(data: web::Json<Request>, app_state: web::Data<AppState>) -> Serv
|
||||
{
|
||||
Ok(res) => {
|
||||
if !res.status().is_success() {
|
||||
return ErrorCode::VkIdError.into_response();
|
||||
return Err(ErrorCode::VkIdError).into();
|
||||
}
|
||||
|
||||
match res.json::<VkIdAuthResponse>().await {
|
||||
Ok(auth_data) =>
|
||||
Ok(Response {
|
||||
access_token: auth_data.id_token,
|
||||
}).into(),
|
||||
Ok(auth_data) => Ok(Response {
|
||||
access_token: auth_data.id_token,
|
||||
}),
|
||||
Err(error) => {
|
||||
sentry::capture_error(&error);
|
||||
|
||||
ErrorCode::VkIdError.into_response()
|
||||
|
||||
Err(ErrorCode::VkIdError)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => ErrorCode::VkIdError.into_response(),
|
||||
Err(_) => Err(ErrorCode::VkIdError),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use actix_macros::{ErrResponse, OkResponse};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -97,7 +97,7 @@ mod schema {
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, OkResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(as = VkIdOAuth::Response)]
|
||||
pub struct Response {
|
||||
@@ -105,7 +105,7 @@ mod schema {
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, IntoResponseErrorNamed, StatusCode, Display)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = VkIdOAuth::ErrorCode)]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||
|
||||
Reference in New Issue
Block a user