diff --git a/Cargo.lock b/Cargo.lock index 7a5257d..3805402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1234,6 +1234,7 @@ dependencies = [ "migration", "paste", "sea-orm", + "serde", ] [[package]] diff --git a/database/Cargo.toml b/database/Cargo.toml index ede4867..c78d7f0 100644 --- a/database/Cargo.toml +++ b/database/Cargo.toml @@ -8,4 +8,5 @@ migration = { path = "migration" } entity = { path = "entity" } sea-orm = { version = "2.0.0-rc.15", features = ["sqlx-postgres", "runtime-tokio"] } -paste = "1" \ No newline at end of file +paste = "1" +serde = { version = "1", features = ["derive"] } \ No newline at end of file diff --git a/database/src/lib.rs b/database/src/lib.rs index e3bd1cd..ad822c6 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -4,6 +4,8 @@ pub use migration; pub use sea_orm; pub mod entity { + use serde::{Deserialize, Serialize}; + pub use entity::*; pub use entity::user::{ @@ -19,4 +21,11 @@ pub mod entity { Entity as ServiceUserEntity, // Model as ServiceUser, // }; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + pub enum UserType { + Default, + Service, + } } diff --git a/providers/provider-engels-polytechnic/src/parser/worksheet.rs b/providers/provider-engels-polytechnic/src/parser/worksheet.rs index 0ad5573..94d626c 100644 --- a/providers/provider-engels-polytechnic/src/parser/worksheet.rs +++ b/providers/provider-engels-polytechnic/src/parser/worksheet.rs @@ -25,7 +25,7 @@ fn format_column_index(index: u32) -> String { return format!("{}{}", format_column_index(quotient - 1), char); } - return char.to_string(); + char.to_string() } impl Display for CellPos { diff --git a/src/extractors/authorized_user.rs b/src/extractors/authorized_user.rs index 4dc5433..95455f6 100644 --- a/src/extractors/authorized_user.rs +++ b/src/extractors/authorized_user.rs @@ -1,12 +1,12 @@ use crate::extractors::base::FromRequestAsync; use crate::state::AppState; -use crate::utility::jwt; +use crate::utility::req_auth; +use crate::utility::req_auth::get_claims_from_req; use actix_macros::MiddlewareError; use actix_web::body::BoxBody; use actix_web::dev::Payload; -use actix_web::http::header; use actix_web::{web, HttpRequest}; -use database::entity::User; +use database::entity::{User, UserType}; use database::query::Query; use derive_more::Display; use serde::{Deserialize, Serialize}; @@ -28,80 +28,53 @@ pub enum Error { #[display("Invalid or expired access token")] InvalidAccessToken, + /// Default user is required. + #[display("Non-default user type is owning this access token")] + #[status_code = "actix_web::http::StatusCode::FORBIDDEN"] + NonDefaultUserType, + /// The user bound to the token is not found in the database. #[display("No user associated with access token")] NoUser, + + /// User doesn't have required role. + #[display("You don't have sufficient rights")] + #[status_code = "actix_web::http::StatusCode::FORBIDDEN"] + InsufficientRights, } -impl Error { - pub fn into_err(self) -> actix_web::Error { - actix_web::Error::from(self) +impl From for Error { + fn from(value: req_auth::Error) -> Self { + match value { + req_auth::Error::NoHeaderOrCookieFound => Error::NoHeaderOrCookieFound, + req_auth::Error::UnknownAuthorizationType => Error::UnknownAuthorizationType, + req_auth::Error::InvalidAccessToken => Error::InvalidAccessToken, + } } } -fn get_access_token_from_header(req: &HttpRequest) -> Result { - let header_value = req - .headers() - .get(header::AUTHORIZATION) - .ok_or(Error::NoHeaderOrCookieFound)? - .to_str() - .map_err(|_| Error::NoHeaderOrCookieFound)? - .to_string(); - - let parts = header_value - .split_once(' ') - .ok_or(Error::UnknownAuthorizationType)?; - - if parts.0 != "Bearer" { - Err(Error::UnknownAuthorizationType) - } else { - Ok(parts.1.to_string()) - } -} - -fn get_access_token_from_cookies(req: &HttpRequest) -> Result { - let cookie = req - .cookie("access_token") - .ok_or(Error::NoHeaderOrCookieFound)?; - - Ok(cookie.value().to_string()) -} - /// User extractor from request with Bearer access token. impl FromRequestAsync for User { - type Error = actix_web::Error; + type Error = Error; async fn from_request_async( req: &HttpRequest, _payload: &mut Payload, ) -> Result { - let access_token = match get_access_token_from_header(req) { - Err(Error::NoHeaderOrCookieFound) => { - get_access_token_from_cookies(req).map_err(|error| error.into_err())? - } - Err(error) => { - return Err(error.into_err()); - } - Ok(access_token) => access_token, - }; + let claims = get_claims_from_req(req).map_err(Error::from)?; - let user_id = jwt::verify_and_decode(&access_token) - .map_err(|_| Error::InvalidAccessToken.into_err())?; + if claims.user_type.unwrap_or(UserType::Default) != UserType::Default { + return Err(Error::NonDefaultUserType); + } let db = req .app_data::>() .unwrap() .get_database(); - Query::find_user_by_id(db, &user_id) - .await - .map_err(|_| Error::NoUser.into()) - .and_then(|user| { - if let Some(user) = user { - Ok(user) - } else { - Err(actix_web::Error::from(Error::NoUser)) - } - }) + match Query::find_user_by_id(db, &claims.id).await { + Ok(Some(user)) => Ok(user), + _ => Err(Error::NoUser), + } } } diff --git a/src/main.rs b/src/main.rs index e0d8017..e830673 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ -use crate::middlewares::authorization::JWTAuthorization; +use crate::middlewares::authorization::{JWTAuthorizationBuilder, ServiceConfig}; use crate::middlewares::content_type::ContentTypeBootstrap; use crate::state::{new_app_state, AppState}; use actix_web::dev::{ServiceFactory, ServiceRequest}; use actix_web::{App, Error, HttpServer}; +use database::entity::sea_orm_active_enums::UserRole; use dotenvy::dotenv; use log::info; use std::io; @@ -26,6 +27,22 @@ pub fn get_api_scope< >( scope: I, ) -> Scope { + let admin_scope = { + let service_user_scope = + utoipa_actix_web::scope("/service-users").service(routes::admin::service_users::create); + + utoipa_actix_web::scope("/admin") + .wrap( + JWTAuthorizationBuilder::new() + .with_default(Some(ServiceConfig { + allow_service: false, + user_roles: Some(&[UserRole::Admin]), + })) + .build(), + ) + .service(service_user_scope) + }; + let auth_scope = utoipa_actix_web::scope("/auth") .service(routes::auth::sign_in) .service(routes::auth::sign_in_vk) @@ -33,26 +50,49 @@ pub fn get_api_scope< .service(routes::auth::sign_up_vk); let users_scope = utoipa_actix_web::scope("/users") - .wrap(JWTAuthorization::default()) + .wrap(JWTAuthorizationBuilder::new().build()) .service(routes::users::change_group) .service(routes::users::change_username) .service(routes::users::me); let schedule_scope = utoipa_actix_web::scope("/schedule") - .wrap(JWTAuthorization { - ignore: &["/group-names", "/teacher-names"], - }) - .service(routes::schedule::schedule) + .wrap( + JWTAuthorizationBuilder::new() + .with_default(Some(ServiceConfig { + allow_service: true, + user_roles: None, + })) + .add_paths(["/group-names", "/teacher-names"], None) + .add_paths( + ["/"], + Some(ServiceConfig { + allow_service: true, + user_roles: Some(&[UserRole::Admin]), + }), + ) + .add_paths( + ["/group"], + Some(ServiceConfig { + allow_service: false, + user_roles: None, + }), + ) + .build(), + ) .service(routes::schedule::cache_status) + .service(routes::schedule::schedule) .service(routes::schedule::group) + .service(routes::schedule::group_by_name) .service(routes::schedule::group_names) .service(routes::schedule::teacher) .service(routes::schedule::teacher_names); let flow_scope = utoipa_actix_web::scope("/flow") - .wrap(JWTAuthorization { - ignore: &["/telegram-auth"], - }) + .wrap( + JWTAuthorizationBuilder::new() + .add_paths(["/telegram-auth"], None) + .build(), + ) .service(routes::flow::telegram_auth) .service(routes::flow::telegram_complete); @@ -60,6 +100,7 @@ pub fn get_api_scope< .service(routes::vk_id::oauth); utoipa_actix_web::scope(scope) + .service(admin_scope) .service(auth_scope) .service(users_scope) .service(schedule_scope) diff --git a/src/middlewares/authorization.rs b/src/middlewares/authorization.rs index 90835b1..46a3665 100644 --- a/src/middlewares/authorization.rs +++ b/src/middlewares/authorization.rs @@ -1,18 +1,64 @@ use crate::extractors::authorized_user; -use crate::extractors::base::FromRequestAsync; +use crate::state::AppState; +use crate::utility::req_auth::get_claims_from_req; use actix_web::body::{BoxBody, EitherBody}; -use actix_web::dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::{Error, HttpRequest, ResponseError}; -use database::entity::User; +use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::{web, Error, HttpRequest, ResponseError}; +use database::entity::sea_orm_active_enums::UserRole; +use database::query::Query; use futures_util::future::LocalBoxFuture; use std::future::{ready, Ready}; +use std::ops::Deref; use std::rc::Rc; +use std::sync::Arc; +use database::entity::UserType; + +#[derive(Default, Clone)] +pub struct ServiceConfig { + /// Allow service users to access endpoints. + pub allow_service: bool, + + /// List of required roles to access endpoints. + pub user_roles: Option<&'static [UserRole]>, +} + +type ServiceKV = (Arc<[&'static str]>, Option); + +pub struct JWTAuthorizationBuilder { + pub default_config: Option, + pub path_configs: Vec, +} + +impl JWTAuthorizationBuilder { + pub fn new() -> Self { + JWTAuthorizationBuilder { + default_config: Some(ServiceConfig::default()), + path_configs: vec![], + } + } + + pub fn with_default(mut self, default: Option) -> Self { + self.default_config = default; + self + } + + pub fn add_paths(mut self, paths: impl AsRef<[&'static str]>, config: Option) -> Self { + self.path_configs.push((Arc::from(paths.as_ref()), config)); + self + } + + pub fn build(self) -> JWTAuthorization { + JWTAuthorization { + default_config: Arc::new(self.default_config), + path_configs: Arc::from(self.path_configs), + } + } +} /// Middleware guard working with JWT tokens. -#[derive(Default)] pub struct JWTAuthorization { - /// List of ignored endpoints. - pub ignore: &'static [&'static str], + pub default_config: Arc>, + pub path_configs: Arc<[ServiceKV]>, } impl Transform for JWTAuthorization @@ -30,15 +76,17 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(JWTAuthorizationMiddleware { service: Rc::new(service), - ignore: self.ignore, + default_config: self.default_config.clone(), + path_configs: self.path_configs.clone(), })) } } pub struct JWTAuthorizationMiddleware { service: Rc, - /// List of ignored endpoints. - ignore: &'static [&'static str], + + default_config: Arc>, + path_configs: Arc<[ServiceKV]>, } impl JWTAuthorizationMiddleware @@ -48,29 +96,68 @@ where B: 'static, { /// Checking the validity of the token. - async fn check_authorization(req: &HttpRequest) -> Result<(), authorized_user::Error> { - let mut payload = Payload::None; + async fn check_authorization( + req: &HttpRequest, + allow_service_user: bool, + required_user_roles: Option<&'static [UserRole]>, + ) -> Result<(), authorized_user::Error> { + let claims = get_claims_from_req(req).map_err(authorized_user::Error::from)?; - User::from_request_async(req, &mut payload) - .await - .map(|_| ()) - .map_err(|e| e.as_error::().unwrap().clone()) + let db = req + .app_data::>() + .unwrap() + .get_database(); + + let user_type = claims.user_type.unwrap_or(UserType::Default); + + match user_type { + UserType::Default => { + if let Some(required_user_roles) = required_user_roles { + let Ok(Some(user)) = Query::find_user_by_id(db, &claims.id).await else { + return Err(authorized_user::Error::NoUser); + }; + + if !required_user_roles.contains(&user.role) { + return Err(authorized_user::Error::InsufficientRights); + } + + return Ok(()); + } + + match Query::is_user_exists_by_id(db, &claims.id).await { + Ok(true) => Ok(()), + _ => Err(authorized_user::Error::NoUser), + } + } + UserType::Service => { + if !allow_service_user { + return Err(authorized_user::Error::NonDefaultUserType); + } + + match Query::is_service_user_exists_by_id(db, &claims.id).await { + Ok(true) => Ok(()), + _ => Err(authorized_user::Error::NoUser), + } + } + } } - fn should_skip(&self, req: &ServiceRequest) -> bool { - let path = req.match_info().unprocessed(); + fn find_config( + current_path: &str, + per_route: &[ServiceKV], + default: &Option, + ) -> Option { + for (service_paths, config) in per_route { + for service_path in service_paths.deref() { + if !service_path.eq(¤t_path) { + continue; + } - self.ignore.iter().any(|ignore| { - if !path.starts_with(ignore) { - return false; + return config.clone(); } + } - if let Some(other) = path.as_bytes().get(ignore.len()) { - return [b'?', b'/'].contains(other); - } - - true - }) + default.clone() } } @@ -87,15 +174,24 @@ where forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { - if self.should_skip(&req) { - let fut = self.service.call(req); - return Box::pin(async move { Ok(fut.await?.map_into_left_body()) }); - } - let service = Rc::clone(&self.service); + let Some(config) = Self::find_config( + req.match_info().unprocessed(), + &self.path_configs, + &self.default_config, + ) else { + let fut = self.service.call(req); + return Box::pin(async move { Ok(fut.await?.map_into_left_body()) }); + }; + + let allow_service_user = config.allow_service; + let required_user_roles = config.user_roles; + Box::pin(async move { - match Self::check_authorization(req.request()).await { + match Self::check_authorization(req.request(), allow_service_user, required_user_roles) + .await + { Ok(_) => { let fut = service.call(req).await?; Ok(fut.map_into_left_body()) diff --git a/src/routes/admin/mod.rs b/src/routes/admin/mod.rs new file mode 100644 index 0000000..dfde440 --- /dev/null +++ b/src/routes/admin/mod.rs @@ -0,0 +1 @@ +pub mod service_users; diff --git a/src/routes/admin/service_users/create.rs b/src/routes/admin/service_users/create.rs new file mode 100644 index 0000000..ffaf72b --- /dev/null +++ b/src/routes/admin/service_users/create.rs @@ -0,0 +1,75 @@ +use self::schema::*; +use crate::{utility, AppState}; +use actix_web::{post, web}; +use database::entity::{ActiveServiceUser, UserType}; +use database::query::Query; +use database::sea_orm::{ActiveModelTrait, Set}; +use objectid::ObjectId; +use web::Json; + +#[utoipa::path(responses( + (status = OK, body = Response), +))] +#[post("/create")] +pub async fn create(data_json: Json, app_state: web::Data) -> ServiceResponse { + let service_user = + match Query::find_service_user_by_id(app_state.get_database(), &data_json.name) + .await + .expect("Failed to find service user by name") + { + Some(_) => return Err(ErrorCode::AlreadyExists).into(), + None => { + let new_user = ActiveServiceUser { + id: Set(ObjectId::new().unwrap().to_string()), + name: Set(data_json.name.clone()), + }; + + new_user + .insert(app_state.get_database()) + .await + .expect("Failed to insert service user") + } + }; + + let access_token = utility::jwt::encode(UserType::Service, &service_user.id); + Ok(Response::new(access_token)).into() +} + +mod schema { + use actix_macros::{ErrResponse, OkResponse}; + use derive_more::Display; + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + #[serde(rename_all = "camelCase")] + #[schema(as = ServiceUser::Create::Request)] + pub struct Request { + /// Service username. + pub name: String, + } + + #[derive(Serialize, ToSchema, OkResponse)] + #[serde(rename_all = "camelCase")] + #[schema(as = ServiceUser::Create::Response)] + pub struct Response { + access_token: String, + } + + impl Response { + pub fn new(access_token: String) -> Self { + Self { access_token } + } + } + + pub type ServiceResponse = crate::routes::schema::Response; + + #[derive(Clone, ToSchema, Display, ErrResponse, Serialize)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + #[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"] + #[schema(as = ServiceUser::Create::ErrorCode)] + pub enum ErrorCode { + #[display("Service user with that name already exists.")] + AlreadyExists, + } +} diff --git a/src/routes/admin/service_users/mod.rs b/src/routes/admin/service_users/mod.rs new file mode 100644 index 0000000..63fc9a4 --- /dev/null +++ b/src/routes/admin/service_users/mod.rs @@ -0,0 +1,3 @@ +mod create; + +pub use create::*; \ No newline at end of file diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index f8d9044..79f0fc4 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -7,6 +7,7 @@ use crate::{utility, AppState}; use actix_web::{post, web}; use database::query::Query; use web::Json; +use database::entity::UserType; async fn sign_in_combined( data: SignInData, @@ -40,7 +41,7 @@ async fn sign_in_combined( } } - let access_token = utility::jwt::encode(&user.id); + let access_token = utility::jwt::encode(UserType::Default, &user.id); Ok(UserResponse::from_user_with_token(user, access_token)) } @@ -184,9 +185,7 @@ mod tests { let active_user = ActiveUser { id: Set(id.clone()), username: Set(username), - password: Set(Some( - bcrypt::hash("example", bcrypt::DEFAULT_COST).unwrap(), - )), + password: Set(Some(bcrypt::hash("example", bcrypt::DEFAULT_COST).unwrap())), vk_id: Set(None), telegram_id: Set(None), group: Set(Some("ИС-214/23".to_string())), diff --git a/src/routes/auth/sign_up.rs b/src/routes/auth/sign_up.rs index 82a8274..057b521 100644 --- a/src/routes/auth/sign_up.rs +++ b/src/routes/auth/sign_up.rs @@ -5,7 +5,7 @@ use crate::routes::schema::ResponseError; use crate::{utility, AppState}; use actix_web::{post, web}; use database::entity::sea_orm_active_enums::UserRole; -use database::entity::ActiveUser; +use database::entity::{ActiveUser, UserType}; use database::query::Query; use database::sea_orm::ActiveModelTrait; use web::Json; @@ -51,7 +51,7 @@ async fn sign_up_combined( let active_user: ActiveUser = data.into(); let user = active_user.insert(db).await.unwrap(); - let access_token = utility::jwt::encode(&user.id); + let access_token = utility::jwt::encode(UserType::Default, &user.id); Ok(UserResponse::from_user_with_token(user, access_token)) } diff --git a/src/routes/flow/telegram_auth.rs b/src/routes/flow/telegram_auth.rs index c25c85a..3fb343d 100644 --- a/src/routes/flow/telegram_auth.rs +++ b/src/routes/flow/telegram_auth.rs @@ -5,7 +5,7 @@ use crate::{utility, AppState}; use actix_web::{post, web}; use chrono::{DateTime, Duration, Utc}; use database::entity::sea_orm_active_enums::UserRole; -use database::entity::ActiveUser; +use database::entity::{ActiveUser, UserType}; use database::query::Query; use database::sea_orm::{ActiveModelTrait, Set}; use objectid::ObjectId; @@ -73,7 +73,7 @@ pub async fn telegram_auth( } }; - let access_token = utility::jwt::encode(&user.id); + let access_token = utility::jwt::encode(UserType::Default, &user.id); Ok(Response::new(&access_token, user.group.is_some())).into() } diff --git a/src/routes/flow/telegram_complete.rs b/src/routes/flow/telegram_complete.rs index d77e52a..667a16e 100644 --- a/src/routes/flow/telegram_complete.rs +++ b/src/routes/flow/telegram_complete.rs @@ -58,10 +58,7 @@ pub async fn telegram_complete( active_user.group = Set(Some(data.group)); - active_user - .update(db) - .await - .expect("Failed to update user"); + active_user.update(db).await.expect("Failed to update user"); Ok(()).into() } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 75bf547..e839988 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,3 +1,4 @@ +pub mod admin; pub mod auth; pub mod flow; pub mod schedule; diff --git a/src/routes/schedule/group_by_name.rs b/src/routes/schedule/group_by_name.rs new file mode 100644 index 0000000..0d5c5e3 --- /dev/null +++ b/src/routes/schedule/group_by_name.rs @@ -0,0 +1,65 @@ +use self::schema::*; +use crate::routes::schedule::schema::ScheduleEntryResponse; +use crate::routes::schema::ResponseError; +use crate::AppState; +use actix_web::{get, web}; + +#[utoipa::path(responses( + (status = OK, body = ScheduleEntryResponse), + ( + status = SERVICE_UNAVAILABLE, + body = ResponseError, + example = json!({ + "code": "NO_SCHEDULE", + "message": "Schedule not parsed yet." + }) + ), + ( + status = NOT_FOUND, + body = ResponseError, + example = json!({ + "code": "NOT_FOUND", + "message": "Required group not found." + }) + ), +))] +#[get("/group/{group_name}")] +pub async fn group_by_name( + path: web::Path, + app_state: web::Data, +) -> ServiceResponse { + let group_name = path.into_inner(); + + match app_state + .get_schedule_snapshot("eng_polytechnic") + .await + .unwrap() + .data + .groups + .get(&group_name) + { + None => Err(ErrorCode::NotFound), + Some(entry) => Ok(entry.clone().into()), + } + .into() +} + +mod schema { + 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; + + #[derive(Clone, Serialize, Display, ToSchema, ErrResponse)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + #[schema(as = GroupByNameSchedule::ErrorCode)] + pub enum ErrorCode { + /// Group not found. + #[status_code = "actix_web::http::StatusCode::NOT_FOUND"] + #[display("Required group not found.")] + NotFound, + } +} diff --git a/src/routes/schedule/mod.rs b/src/routes/schedule/mod.rs index 704750f..9eee2b6 100644 --- a/src/routes/schedule/mod.rs +++ b/src/routes/schedule/mod.rs @@ -1,5 +1,6 @@ mod cache_status; mod group; +mod group_by_name; mod group_names; mod get; mod schema; @@ -8,6 +9,7 @@ mod teacher_names; pub use cache_status::*; pub use group::*; +pub use group_by_name::*; pub use group_names::*; pub use get::*; pub use teacher::*; diff --git a/src/utility/jwt.rs b/src/utility/jwt.rs index 621e63c..b42fbaf 100644 --- a/src/utility/jwt.rs +++ b/src/utility/jwt.rs @@ -1,13 +1,14 @@ use chrono::Duration; use chrono::Utc; use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use serde_with::DisplayFromStr; use serde_with::serde_as; +use serde_with::DisplayFromStr; use std::env; use std::mem::discriminant; use std::sync::LazyLock; +use database::entity::UserType; /// Key for token verification. static DECODING_KEY: LazyLock = LazyLock::new(|| { @@ -42,27 +43,31 @@ impl PartialEq for Error { } } + /// The data the token holds. #[serde_as] #[derive(Debug, Serialize, Deserialize)] -struct Claims { +pub struct Claims { /// User account UUID. - id: String, + pub id: String, + + /// User type. + pub user_type: Option, /// Token creation date. #[serde_as(as = "DisplayFromStr")] - iat: u64, + pub iat: u64, /// Token expiry date. #[serde_as(as = "DisplayFromStr")] - exp: u64, + pub exp: u64, } /// Token signing algorithm. pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256; /// Checking the token and extracting the UUID of the user account from it. -pub fn verify_and_decode(token: &str) -> Result { +pub fn verify_and_decode(token: &str) -> Result { let mut validation = Validation::new(DEFAULT_ALGORITHM); validation.required_spec_claims.remove("exp"); @@ -75,7 +80,7 @@ pub fn verify_and_decode(token: &str) -> Result { if token_data.claims.exp < Utc::now().timestamp().unsigned_abs() { Err(Error::Expired) } else { - Ok(token_data.claims.id) + Ok(token_data.claims) } } Err(err) => Err(match err.into_kind() { @@ -87,7 +92,7 @@ pub fn verify_and_decode(token: &str) -> Result { } /// Creating a user token. -pub fn encode(id: &str) -> String { +pub fn encode(user_type: UserType, id: &str) -> String { let header = Header { typ: Some(String::from("JWT")), ..Default::default() @@ -98,6 +103,7 @@ pub fn encode(id: &str) -> String { let claims = Claims { id: id.to_string(), + user_type: Some(user_type), iat: iat.timestamp().unsigned_abs(), exp: exp.timestamp().unsigned_abs(), }; @@ -114,7 +120,7 @@ mod tests { fn test_encode() { test_env(); - assert!(!encode("test").is_empty()); + assert!(!encode(UserType::Default, "test").is_empty()); } #[test] @@ -125,10 +131,7 @@ mod tests { let result = verify_and_decode(&token); assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - Error::InvalidToken - ); + assert_eq!(result.err().unwrap(), Error::InvalidToken); } //noinspection SpellCheckingInspection diff --git a/src/utility/mod.rs b/src/utility/mod.rs index 8d13d76..c3f2bcd 100644 --- a/src/utility/mod.rs +++ b/src/utility/mod.rs @@ -1,2 +1,3 @@ pub mod jwt; pub mod telegram; +pub mod req_auth; diff --git a/src/utility/req_auth.rs b/src/utility/req_auth.rs new file mode 100644 index 0000000..81ccb37 --- /dev/null +++ b/src/utility/req_auth.rs @@ -0,0 +1,56 @@ +use crate::utility::jwt; +use crate::utility::jwt::Claims; +use actix_web::http::header; +use actix_web::HttpRequest; + +#[derive(Debug, PartialEq)] +pub enum Error { + /// There is no Authorization header or cookie in the request. + NoHeaderOrCookieFound, + + /// Unknown authorization type other than Bearer. + UnknownAuthorizationType, + + /// Invalid or expired access token. + InvalidAccessToken, +} + +pub fn get_access_token_from_header(req: &HttpRequest) -> Result { + let header_value = req + .headers() + .get(header::AUTHORIZATION) + .ok_or(Error::NoHeaderOrCookieFound)? + .to_str() + .map_err(|_| Error::NoHeaderOrCookieFound)? + .to_string(); + + let parts = header_value + .split_once(' ') + .ok_or(Error::UnknownAuthorizationType)?; + + if parts.0 != "Bearer" { + Err(Error::UnknownAuthorizationType) + } else { + Ok(parts.1.to_string()) + } +} + +pub fn get_access_token_from_cookies(req: &HttpRequest) -> Result { + let cookie = req + .cookie("access_token") + .ok_or(Error::NoHeaderOrCookieFound)?; + + Ok(cookie.value().to_string()) +} + +pub fn get_claims_from_req(req: &HttpRequest) -> Result { + let access_token = match get_access_token_from_header(req) { + Err(Error::NoHeaderOrCookieFound) => get_access_token_from_cookies(req)?, + Err(error) => { + return Err(error); + } + Ok(access_token) => access_token, + }; + + jwt::verify_and_decode(&access_token).map_err(|_| Error::InvalidAccessToken) +}