diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15f10fa..7da61b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,6 @@ jobs: run: cargo test env: DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} - JWT_SECRET: "test-secret-at-least-256-bits-used" \ No newline at end of file + JWT_SECRET: "test-secret-at-least-256-bits-used" + VKID_CLIENT_ID: 0 + VKID_REDIRECT_URI: "vk0://vk.com/blank.html" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f2dd7bb..aed6cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,7 @@ dependencies = [ "utoipa", "utoipa-actix-web", "utoipa-rapidoc", + "uuid", ] [[package]] @@ -2981,6 +2982,15 @@ dependencies = [ "utoipa", ] +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 8cf27b1..1ec0da9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ hex = "0.4.3" mime = "0.3.17" objectid = "0.2.0" regex = "1.11.1" -reqwest = "0.12.15" +reqwest = { version = "0.12.15", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" serde_with = "3.12.0" @@ -36,6 +36,7 @@ rand = "0.9.0" utoipa = { version = "5", features = ["actix_extras", "chrono"] } utoipa-rapidoc = { version = "6.0.0", features = ["actix-web"] } utoipa-actix-web = "0.1" +uuid = { version = "1.16.0", features = ["v4"] } [dev-dependencies] actix-test = { path = "actix-test" } diff --git a/migrations/2025-03-21-212111_create_users/up.sql b/migrations/2025-03-21-212111_create_users/up.sql index ca1ac7e..ed2045e 100644 --- a/migrations/2025-03-21-212111_create_users/up.sql +++ b/migrations/2025-03-21-212111_create_users/up.sql @@ -2,7 +2,7 @@ CREATE TABLE users ( id text PRIMARY KEY NOT NULL, username text UNIQUE NOT NULL, - "password" text NOT NULL, + password text NOT NULL, vk_id int4 NULL, access_token text UNIQUE NOT NULL, "group" text NOT NULL, diff --git a/migrations/2025-03-21-212723_create_fcm/up.sql b/migrations/2025-03-21-212723_create_fcm/up.sql index 77dd7da..218a90d 100644 --- a/migrations/2025-03-21-212723_create_fcm/up.sql +++ b/migrations/2025-03-21-212723_create_fcm/up.sql @@ -1,11 +1,6 @@ CREATE TABLE fcm ( - user_id text PRIMARY KEY NOT NULL, + user_id text PRIMARY KEY NOT NULL REFERENCES users (id), token text NOT NULL, - topics text[] NULL -); - -CREATE UNIQUE INDEX fcm_user_id_key ON fcm USING btree (user_id); - -ALTER TABLE fcm - ADD CONSTRAINT fcm_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE; \ No newline at end of file + topics text[] NOT NULL +); \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index 1a6e398..733575e 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,6 +7,7 @@ use diesel::{Connection, PgConnection}; use sha1::{Digest, Sha1}; use std::env; use std::hash::Hash; +use std::ops::DerefMut; use std::sync::{Mutex, MutexGuard}; #[derive(Clone)] @@ -18,6 +19,24 @@ pub struct Schedule { pub data: ParseResult, } +#[derive(Clone)] +pub struct VkId { + pub client_id: i32, + pub redirect_url: String, +} + +impl VkId { + pub fn new() -> Self { + Self { + client_id: env::var("VKID_CLIENT_ID") + .expect("VKID_CLIENT_ID must be set") + .parse() + .expect("VKID_CLIENT_ID must be integer"), + redirect_url: env::var("VKID_REDIRECT_URI").expect("VKID_REDIRECT_URI must be set"), + } + } +} + impl Schedule { pub fn hash(&self) -> String { let mut hasher = DigestHasher::from(Sha1::new()); @@ -36,6 +55,23 @@ pub struct AppState { pub downloader: Mutex, pub schedule: Mutex>, pub database: Mutex, + pub vk_id: VkId, +} + +impl AppState { + pub fn new() -> Self { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + Self { + downloader: Mutex::new(BasicXlsDownloader::new()), + schedule: Mutex::new(None), + database: Mutex::new( + PgConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)), + ), + vk_id: VkId::new(), + } + } } impl AppState { @@ -43,18 +79,19 @@ impl AppState { pub fn connection(&self) -> MutexGuard { self.database.lock().unwrap() } + + pub fn lock_connection(&self, f: F) -> T + where + F: FnOnce(&mut PgConnection) -> T, + { + let mut lock = self.connection(); + let conn = lock.deref_mut(); + + f(conn) + } } /// Создание нового объекта web::Data pub fn app_state() -> web::Data { - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - - web::Data::new(AppState { - downloader: Mutex::new(BasicXlsDownloader::new()), - schedule: Mutex::new(None), - database: Mutex::new( - PgConnection::establish(&database_url) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)), - ), - }) + web::Data::new(AppState::new()) } diff --git a/src/database/driver.rs b/src/database/driver.rs index 0aa1dba..07464e7 100644 --- a/src/database/driver.rs +++ b/src/database/driver.rs @@ -2,7 +2,7 @@ pub mod users { use crate::database::models::User; use crate::database::schema::users::dsl::users; use crate::database::schema::users::dsl::*; - use diesel::{ExpressionMethods, QueryResult, insert_into}; + use diesel::{insert_into, ExpressionMethods, QueryResult}; use diesel::{PgConnection, SelectableHelper}; use diesel::{QueryDsl, RunQueryDsl}; use std::ops::DerefMut; @@ -31,10 +31,7 @@ pub mod users { .first(con) } - pub fn get_by_vk_id( - connection: &Mutex, - _vk_id: i32, - ) -> QueryResult { + pub fn get_by_vk_id(connection: &Mutex, _vk_id: i32) -> QueryResult { let mut lock = connection.lock().unwrap(); let con = lock.deref_mut(); @@ -89,7 +86,7 @@ pub mod users { Err(_) => false, } } - + #[cfg(test)] pub fn insert_or_ignore(connection: &Mutex, user: &User) -> QueryResult { let mut lock = connection.lock().unwrap(); @@ -101,3 +98,21 @@ pub mod users { .execute(con) } } + +pub mod fcm { + use crate::database::models::{User, FCM}; + use diesel::QueryDsl; + use diesel::RunQueryDsl; + use diesel::{BelongingToDsl, PgConnection, QueryResult, SelectableHelper}; + use std::ops::DerefMut; + use std::sync::Mutex; + + pub fn from_user(connection: &Mutex, user: &User) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + FCM::belonging_to(&user) + .select(FCM::as_select()) + .get_result(con) + } +} diff --git a/src/database/models.rs b/src/database/models.rs index 8b33c96..2d4ee55 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,16 +1,11 @@ use actix_macros::ResponderJson; +use diesel::QueryId; use diesel::prelude::*; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; #[derive( - diesel_derive_enum::DbEnum, - Serialize, - Deserialize, - Debug, - Clone, - Copy, - PartialEq, - utoipa::ToSchema, + Copy, Clone, PartialEq, Debug, Serialize, Deserialize, diesel_derive_enum::DbEnum, ToSchema, )] #[ExistingTypePath = "crate::database::schema::sql_types::UserRole"] #[DbValueStyle = "UPPERCASE"] @@ -25,11 +20,12 @@ pub enum UserRole { Identifiable, AsChangeset, Queryable, + QueryId, Selectable, Serialize, Insertable, Debug, - utoipa::ToSchema, + ToSchema, ResponderJson, )] #[diesel(table_name = crate::database::schema::users)] @@ -59,3 +55,29 @@ pub struct User { /// Версия установленного приложения Polytechnic+ pub version: String, } + +#[derive( + Debug, + Serialize, + Identifiable, + Queryable, + Selectable, + Insertable, + AsChangeset, + Associations, + ToSchema, + ResponderJson, +)] +#[diesel(belongs_to(User))] +#[diesel(table_name = crate::database::schema::fcm)] +#[diesel(primary_key(user_id))] +pub struct FCM { + /// UUID аккаунта. + pub user_id: String, + + /// FCM токен. + pub token: String, + + /// Список топиков, на которые подписан пользователь. + pub topics: Vec>, +} diff --git a/src/database/schema.rs b/src/database/schema.rs index ae691df..5eaed50 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -10,7 +10,7 @@ diesel::table! { fcm (user_id) { user_id -> Text, token -> Text, - topics -> Nullable>>, + topics -> Array>, } } diff --git a/src/extractors/authorized_user.rs b/src/extractors/authorized_user.rs index befa422..abb32e3 100644 --- a/src/extractors/authorized_user.rs +++ b/src/extractors/authorized_user.rs @@ -1,13 +1,13 @@ use crate::app_state::AppState; use crate::database::driver; -use crate::database::models::User; -use crate::extractors::base::FromRequestSync; +use crate::database::models::{FCM, User}; +use crate::extractors::base::{FromRequestSync, SyncExtractor}; use crate::utility::jwt; use actix_macros::ResponseErrorMessage; use actix_web::body::BoxBody; use actix_web::dev::Payload; use actix_web::http::header; -use actix_web::{HttpRequest, web}; +use actix_web::{FromRequest, HttpRequest, web}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -66,3 +66,45 @@ impl FromRequestSync for User { driver::users::get(&app_state.database, &user_id).map_err(|_| Error::NoUser.into()) } } + +pub struct UserExtractor { + user: User, + + fcm: Option, +} + +impl UserExtractor<{ FCM }> { + pub fn user(&self) -> &User { + &self.user + } + + pub fn fcm(&self) -> &Option { + if !FCM { + panic!("FCM marked as not required, but it has been requested") + } + + &self.fcm + } +} + +/// Экстрактор пользователя и дополнительных параметров из запроса с токеном +impl FromRequestSync for UserExtractor<{ FCM }> { + type Error = actix_web::Error; + + fn from_request_sync(req: &HttpRequest, payload: &mut Payload) -> Result { + let user = SyncExtractor::::from_request(req, payload) + .into_inner()? + .into_inner(); + + let app_state = req.app_data::>().unwrap(); + + Ok(Self { + fcm: if FCM { + driver::fcm::from_user(&app_state.database, &user).ok() + } else { + None + }, + user, + }) + } +} diff --git a/src/main.rs b/src/main.rs index 4c3fd7d..ce1ee0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,10 @@ use crate::app_state::{AppState, app_state}; use crate::middlewares::authorization::JWTAuthorization; -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::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 actix_web::{App, HttpServer}; +use actix_web::dev::{ServiceFactory, ServiceRequest}; +use actix_web::{App, Error, HttpServer}; use dotenvy::dotenv; use utoipa_actix_web::AppExt; +use utoipa_actix_web::scope::Scope; use utoipa_rapidoc::RapiDoc; mod app_state; @@ -30,6 +22,47 @@ mod utility; mod test_env; +pub fn get_api_scope< + I: Into>, + T: ServiceFactory, +>( + scope: I, +) -> Scope { + let auth_scope = utoipa_actix_web::scope("/auth") + .service(routes::auth::sign_in) + .service(routes::auth::sign_in_vk) + .service(routes::auth::sign_up) + .service(routes::auth::sign_up_vk); + + let users_scope = utoipa_actix_web::scope("/users") + .wrap(JWTAuthorization) + .service(routes::users::me); + + let schedule_scope = utoipa_actix_web::scope("/schedule") + .wrap(JWTAuthorization) + .service(routes::schedule::schedule) + .service(routes::schedule::update_download_url) + .service(routes::schedule::cache_status) + .service(routes::schedule::group) + .service(routes::schedule::group_names) + .service(routes::schedule::teacher) + .service(routes::schedule::teacher_names); + + let fcm_scope = utoipa_actix_web::scope("/fcm") + .wrap(JWTAuthorization) + .service(routes::fcm::update_callback); + + let vk_id_scope = utoipa_actix_web::scope("/vkid") // + .service(routes::vk_id::oauth); + + utoipa_actix_web::scope(scope) + .service(auth_scope) + .service(users_scope) + .service(schedule_scope) + .service(fcm_scope) + .service(vk_id_scope) +} + #[actix_web::main] async fn main() { dotenv().ok(); @@ -40,35 +73,10 @@ async fn main() { let app_state = app_state(); HttpServer::new(move || { - let auth_scope = utoipa_actix_web::scope("/auth") - .service(sign_in_default) - .service(sign_in_vk) - .service(sign_up_default) - .service(sign_up_vk); - - let users_scope = utoipa_actix_web::scope("/users") - .wrap(JWTAuthorization) - .service(me); - - let schedule_scope = utoipa_actix_web::scope("/schedule") - .wrap(JWTAuthorization) - .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") - .service(auth_scope) - .service(users_scope) - .service(schedule_scope); - let (app, api) = App::new() .into_utoipa_app() .app_data(app_state.clone()) - .service(api_scope) + .service(get_api_scope("/api/v1")) .split_for_parts(); let rapidoc_service = RapiDoc::with_openapi("/api-docs-json", api).path("/api-docs"); diff --git a/src/routes/auth/mod.rs b/src/routes/auth/mod.rs index 7122840..698dc7d 100644 --- a/src/routes/auth/mod.rs +++ b/src/routes/auth/mod.rs @@ -1,3 +1,8 @@ -pub mod sign_in; -pub mod sign_up; +mod sign_in; +mod sign_up; mod shared; + +pub use sign_in::*; +pub use sign_up::*; + +// TODO: change-password \ No newline at end of file diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index c1cb88d..b5a71d1 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -11,7 +11,7 @@ use diesel::SaveChangesDsl; use std::ops::DerefMut; use web::Json; -async fn sign_in( +async fn sign_in_combined( data: SignInData, app_state: &web::Data, ) -> Result { @@ -55,8 +55,8 @@ async fn sign_in( (status = NOT_ACCEPTABLE, body = ResponseError) ))] #[post("/sign-in")] -pub async fn sign_in_default(data: Json, app_state: web::Data) -> ServiceResponse { - sign_in(Default(data.into_inner()), &app_state).await.into() +pub async fn sign_in(data: Json, app_state: web::Data) -> ServiceResponse { + sign_in_combined(Default(data.into_inner()), &app_state).await.into() } #[utoipa::path(responses( @@ -68,7 +68,7 @@ pub async fn sign_in_vk(data_json: Json, app_state: web::Data sign_in(Vk(id), &app_state).await.into(), + Ok(id) => sign_in_combined(Vk(id), &app_state).await.into(), Err(_) => ErrorCode::InvalidVkAccessToken.into_response(), } } @@ -134,7 +134,7 @@ mod tests { use super::schema::*; use crate::database::driver; use crate::database::models::{User, UserRole}; - use crate::routes::auth::sign_in::sign_in_default; + use crate::routes::auth::sign_in::sign_in; use crate::test_env::tests::{static_app_state, test_app_state, test_env}; use crate::utility; use actix_test::test_app; @@ -146,7 +146,7 @@ mod tests { use std::fmt::Write; async fn sign_in_client(data: Request) -> ServiceResponse { - let app = test_app(test_app_state(), sign_in_default).await; + let app = test_app(test_app_state(), sign_in).await; let req = test::TestRequest::with_uri("/sign-in") .method(Method::POST) diff --git a/src/routes/auth/sign_up.rs b/src/routes/auth/sign_up.rs index fb7c038..b329fa1 100644 --- a/src/routes/auth/sign_up.rs +++ b/src/routes/auth/sign_up.rs @@ -9,7 +9,7 @@ use actix_web::{post, web}; use rand::{Rng, rng}; use web::Json; -async fn sign_up( +async fn sign_up_combined( data: SignUpData, app_state: &web::Data, ) -> Result { @@ -50,13 +50,13 @@ async fn sign_up( (status = NOT_ACCEPTABLE, body = ResponseError) ))] #[post("/sign-up")] -pub async fn sign_up_default( +pub async fn sign_up( data_json: Json, app_state: web::Data, ) -> ServiceResponse { let data = data_json.into_inner(); - sign_up( + sign_up_combined( SignUpData { username: data.username, password: data.password, @@ -83,7 +83,7 @@ pub async fn sign_up_vk( let data = data_json.into_inner(); match parse_vk_id(&data.access_token) { - Ok(id) => sign_up( + Ok(id) => sign_up_combined( SignUpData { username: data.username, password: rng() @@ -243,7 +243,7 @@ mod tests { use crate::database::driver; use crate::database::models::UserRole; use crate::routes::auth::sign_up::schema::Request; - use crate::routes::auth::sign_up::sign_up_default; + use crate::routes::auth::sign_up::sign_up; use crate::test_env::tests::{static_app_state, test_app_state, test_env}; use actix_test::test_app; use actix_web::dev::ServiceResponse; @@ -258,7 +258,7 @@ mod tests { } async fn sign_up_client(data: SignUpPartial) -> ServiceResponse { - let app = test_app(test_app_state(), sign_up_default).await; + let app = test_app(test_app_state(), sign_up).await; let req = test::TestRequest::with_uri("/sign-up") .method(Method::POST) diff --git a/src/routes/fcm/mod.rs b/src/routes/fcm/mod.rs new file mode 100644 index 0000000..c7b4780 --- /dev/null +++ b/src/routes/fcm/mod.rs @@ -0,0 +1,3 @@ +mod update_callback; + +pub use update_callback::*; diff --git a/src/routes/fcm/update_callback.rs b/src/routes/fcm/update_callback.rs new file mode 100644 index 0000000..72e126f --- /dev/null +++ b/src/routes/fcm/update_callback.rs @@ -0,0 +1,28 @@ +use crate::app_state::AppState; +use crate::database::models::User; +use crate::extractors::base::SyncExtractor; +use actix_web::{HttpResponse, Responder, post, web}; +use diesel::SaveChangesDsl; + +#[utoipa::path(responses( + (status = OK), + (status = INTERNAL_SERVER_ERROR) +))] +#[post("/update-callback/{version}")] +async fn update_callback( + app_state: web::Data, + version: web::Path, + user: SyncExtractor, +) -> impl Responder { + let mut user = user.into_inner(); + + user.version = version.into_inner(); + + match app_state.lock_connection(|con| user.save_changes::(con)) { + Ok(_) => HttpResponse::Ok(), + Err(e) => { + eprintln!("Failed to update user: {}", e); + HttpResponse::InternalServerError() + } + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 9fae8c0..05479c6 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,6 @@ pub mod auth; -pub mod users; +pub mod fcm; pub mod schedule; mod schema; +pub mod users; +pub mod vk_id; diff --git a/src/routes/schedule/get_cache_status.rs b/src/routes/schedule/cache_status.rs similarity index 86% rename from src/routes/schedule/get_cache_status.rs rename to src/routes/schedule/cache_status.rs index fe9906e..aef1d46 100644 --- a/src/routes/schedule/get_cache_status.rs +++ b/src/routes/schedule/cache_status.rs @@ -6,7 +6,7 @@ use actix_web::{get, web}; (status = OK, body = CacheStatus), ))] #[get("/cache-status")] -pub async fn get_cache_status(app_state: web::Data) -> CacheStatus { +pub async fn cache_status(app_state: web::Data) -> CacheStatus { // Prevent thread lock let has_schedule = app_state .schedule diff --git a/src/routes/schedule/get_group.rs b/src/routes/schedule/group.rs similarity index 99% rename from src/routes/schedule/get_group.rs rename to src/routes/schedule/group.rs index 361f169..f40d2b3 100644 --- a/src/routes/schedule/get_group.rs +++ b/src/routes/schedule/group.rs @@ -25,7 +25,7 @@ use actix_web::{get, web}; ), ))] #[get("/group")] -pub async fn get_group( +pub async fn group( user: SyncExtractor, app_state: web::Data, ) -> ServiceResponse { diff --git a/src/routes/schedule/get_group_names.rs b/src/routes/schedule/group_names.rs similarity index 94% rename from src/routes/schedule/get_group_names.rs rename to src/routes/schedule/group_names.rs index 0b82170..dbf7520 100644 --- a/src/routes/schedule/get_group_names.rs +++ b/src/routes/schedule/group_names.rs @@ -9,7 +9,7 @@ use actix_web::{get, web}; (status = SERVICE_UNAVAILABLE, body = ResponseError), ))] #[get("/group-names")] -pub async fn get_group_names(app_state: web::Data) -> ServiceResponse { +pub async fn group_names(app_state: web::Data) -> ServiceResponse { // Prevent thread lock let schedule_lock = app_state.schedule.lock().unwrap(); diff --git a/src/routes/schedule/mod.rs b/src/routes/schedule/mod.rs index d319876..f54ec26 100644 --- a/src/routes/schedule/mod.rs +++ b/src/routes/schedule/mod.rs @@ -1,8 +1,16 @@ -pub mod get_cache_status; -pub mod get_schedule; -pub mod get_group; -pub mod get_group_names; -pub mod get_teacher; -pub mod get_teacher_names; +mod cache_status; +mod group; +mod group_names; +mod schedule; +mod teacher; +mod teacher_names; mod schema; -pub mod update_download_url; +mod update_download_url; + +pub use cache_status::*; +pub use group::*; +pub use group_names::*; +pub use schedule::*; +pub use teacher::*; +pub use teacher_names::*; +pub use update_download_url::*; diff --git a/src/routes/schedule/get_schedule.rs b/src/routes/schedule/schedule.rs similarity index 90% rename from src/routes/schedule/get_schedule.rs rename to src/routes/schedule/schedule.rs index 4db823a..d97933b 100644 --- a/src/routes/schedule/get_schedule.rs +++ b/src/routes/schedule/schedule.rs @@ -9,7 +9,7 @@ use actix_web::{get, web}; (status = SERVICE_UNAVAILABLE, body = ResponseError) ))] #[get("/")] -pub async fn get_schedule(app_state: web::Data) -> ServiceResponse { +pub async fn schedule(app_state: web::Data) -> ServiceResponse { match ScheduleView::try_from(&app_state) { Ok(res) => Ok(res).into(), Err(e) => match e { diff --git a/src/routes/schedule/get_teacher.rs b/src/routes/schedule/teacher.rs similarity index 99% rename from src/routes/schedule/get_teacher.rs rename to src/routes/schedule/teacher.rs index 63bfc70..705b958 100644 --- a/src/routes/schedule/get_teacher.rs +++ b/src/routes/schedule/teacher.rs @@ -23,7 +23,7 @@ use actix_web::{get, web}; ), ))] #[get("/teacher/{name}")] -pub async fn get_teacher( +pub async fn teacher( name: web::Path, app_state: web::Data, ) -> ServiceResponse { diff --git a/src/routes/schedule/get_teacher_names.rs b/src/routes/schedule/teacher_names.rs similarity index 94% rename from src/routes/schedule/get_teacher_names.rs rename to src/routes/schedule/teacher_names.rs index 246400d..788c954 100644 --- a/src/routes/schedule/get_teacher_names.rs +++ b/src/routes/schedule/teacher_names.rs @@ -9,7 +9,7 @@ use actix_web::{get, web}; (status = SERVICE_UNAVAILABLE, body = ResponseError), ))] #[get("/teacher-names")] -pub async fn get_teacher_names(app_state: web::Data) -> ServiceResponse { +pub async fn teacher_names(app_state: web::Data) -> ServiceResponse { // Prevent thread lock let schedule_lock = app_state.schedule.lock().unwrap(); diff --git a/src/routes/users/mod.rs b/src/routes/users/mod.rs index 28a29af..0249b33 100644 --- a/src/routes/users/mod.rs +++ b/src/routes/users/mod.rs @@ -1 +1,6 @@ -pub mod me; \ No newline at end of file +mod me; + +pub use me::*; + +// TODO: change-username +// TODO: change-group \ No newline at end of file diff --git a/src/routes/vk_id/mod.rs b/src/routes/vk_id/mod.rs new file mode 100644 index 0000000..07fcef9 --- /dev/null +++ b/src/routes/vk_id/mod.rs @@ -0,0 +1,3 @@ +mod oauth; + +pub use oauth::*; diff --git a/src/routes/vk_id/oauth.rs b/src/routes/vk_id/oauth.rs new file mode 100644 index 0000000..d50618a --- /dev/null +++ b/src/routes/vk_id/oauth.rs @@ -0,0 +1,114 @@ +use self::schema::*; +use crate::app_state::AppState; +use crate::routes::schema::{IntoResponseAsError, ResponseError}; +use actix_web::{post, web}; +use serde::Deserialize; +use std::collections::HashMap; +use uuid::Uuid; + +#[allow(dead_code)] +#[derive(Deserialize)] +struct VkIdAuthResponse { + refresh_token: String, + access_token: String, + id_token: String, + token_type: String, + expires_in: i32, + user_id: i32, + state: String, + scope: String, +} + +#[utoipa::path(responses( + (status = OK, body = Response), + ( + status = NOT_ACCEPTABLE, + body = ResponseError, + example = json!({ + "code": "VK_ID_ERROR", + "message": "VK server returned an error" + }) + ), +))] +#[post("/oauth")] +async fn oauth(data: web::Json, app_state: web::Data) -> ServiceResponse { + let data = data.into_inner(); + let state = Uuid::new_v4().simple().to_string(); + + let vk_id = &app_state.vk_id; + let client_id = vk_id.client_id.clone().to_string(); + + let mut params = HashMap::new(); + params.insert("grant_type", "authorization_code"); + params.insert("client_id", client_id.as_str()); + params.insert("state", state.as_str()); + params.insert("code_verifier", data.code_verifier.as_str()); + params.insert("code", data.code.as_str()); + params.insert("device_id", data.device_id.as_str()); + params.insert("redirect_uri", vk_id.redirect_url.as_str()); + + let client = reqwest::Client::new(); + match client + .post("https://id.vk.com/oauth2/auth") + .form(¶ms) + .send() + .await + { + Ok(res) => { + if !res.status().is_success() { + return ErrorCode::VkIdError.into_response(); + } + + if let Ok(auth_data) = res.json::().await { + Ok(Response { + access_token: auth_data.id_token, + }) + .into() + } else { + ErrorCode::VkIdError.into_response() + } + } + Err(_) => ErrorCode::VkIdError.into_response(), + } +} + +mod schema { + use actix_macros::{IntoResponseErrorNamed, StatusCode}; + use derive_more::Display; + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + pub type ServiceResponse = crate::routes::schema::Response; + + #[derive(Deserialize, ToSchema)] + #[serde(rename_all = "camelCase")] + #[schema(as = VkIdOAuth::Request)] + pub struct Request { + /// Код подтверждения authorization_code + pub code: String, + + /// Параметр для защиты передаваемых данных + pub code_verifier: String, + + /// Идентификатор устройства + pub device_id: String, + } + + #[derive(Serialize, ToSchema)] + #[serde(rename_all = "camelCase")] + #[schema(as = VkIdOAuth::Response)] + pub struct Response { + /// ID токен + pub access_token: String, + } + + #[derive(Clone, Serialize, ToSchema, IntoResponseErrorNamed, StatusCode, Display)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + #[schema(as = VkIdOAuth::ErrorCode)] + #[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"] + pub enum ErrorCode { + /// Сервер VK вернул ошибку + #[display("VK server returned an error")] + VkIdError, + } +}