mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
feat: implement service users
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1234,6 +1234,7 @@ dependencies = [
|
||||
"migration",
|
||||
"paste",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -9,3 +9,4 @@ entity = { path = "entity" }
|
||||
sea-orm = { version = "2.0.0-rc.15", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||
|
||||
paste = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<req_auth::Error> 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<String, Error> {
|
||||
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<String, Error> {
|
||||
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<Self, Self::Error> {
|
||||
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::<web::Data<AppState>>()
|
||||
.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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
src/main.rs
59
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<T> {
|
||||
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)
|
||||
|
||||
@@ -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<ServiceConfig>);
|
||||
|
||||
pub struct JWTAuthorizationBuilder {
|
||||
pub default_config: Option<ServiceConfig>,
|
||||
pub path_configs: Vec<ServiceKV>,
|
||||
}
|
||||
|
||||
impl JWTAuthorizationBuilder {
|
||||
pub fn new() -> Self {
|
||||
JWTAuthorizationBuilder {
|
||||
default_config: Some(ServiceConfig::default()),
|
||||
path_configs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default(mut self, default: Option<ServiceConfig>) -> Self {
|
||||
self.default_config = default;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_paths(mut self, paths: impl AsRef<[&'static str]>, config: Option<ServiceConfig>) -> 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<Option<ServiceConfig>>,
|
||||
pub path_configs: Arc<[ServiceKV]>,
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> 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<S> {
|
||||
service: Rc<S>,
|
||||
/// List of ignored endpoints.
|
||||
ignore: &'static [&'static str],
|
||||
|
||||
default_config: Arc<Option<ServiceConfig>>,
|
||||
path_configs: Arc<[ServiceKV]>,
|
||||
}
|
||||
|
||||
impl<S, B> JWTAuthorizationMiddleware<S>
|
||||
@@ -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::<authorized_user::Error>().unwrap().clone())
|
||||
let db = req
|
||||
.app_data::<web::Data<AppState>>()
|
||||
.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<ServiceConfig>,
|
||||
) -> Option<ServiceConfig> {
|
||||
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())
|
||||
|
||||
1
src/routes/admin/mod.rs
Normal file
1
src/routes/admin/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod service_users;
|
||||
75
src/routes/admin/service_users/create.rs
Normal file
75
src/routes/admin/service_users/create.rs
Normal file
@@ -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<Request>, app_state: web::Data<AppState>) -> 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<Response, ErrorCode>;
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
3
src/routes/admin/service_users/mod.rs
Normal file
3
src/routes/admin/service_users/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod create;
|
||||
|
||||
pub use create::*;
|
||||
@@ -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())),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
pub mod flow;
|
||||
pub mod schedule;
|
||||
|
||||
65
src/routes/schedule/group_by_name.rs
Normal file
65
src/routes/schedule/group_by_name.rs
Normal file
@@ -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<ErrorCode>,
|
||||
example = json!({
|
||||
"code": "NO_SCHEDULE",
|
||||
"message": "Schedule not parsed yet."
|
||||
})
|
||||
),
|
||||
(
|
||||
status = NOT_FOUND,
|
||||
body = ResponseError<ErrorCode>,
|
||||
example = json!({
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Required group not found."
|
||||
})
|
||||
),
|
||||
))]
|
||||
#[get("/group/{group_name}")]
|
||||
pub async fn group_by_name(
|
||||
path: web::Path<String>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> 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<ScheduleEntryResponse, ErrorCode>;
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<DecodingKey> = 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<UserType>,
|
||||
|
||||
/// 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<String, Error> {
|
||||
pub fn verify_and_decode(token: &str) -> Result<Claims, Error> {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod jwt;
|
||||
pub mod telegram;
|
||||
pub mod req_auth;
|
||||
|
||||
56
src/utility/req_auth.rs
Normal file
56
src/utility/req_auth.rs
Normal file
@@ -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<String, Error> {
|
||||
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<String, Error> {
|
||||
let cookie = req
|
||||
.cookie("access_token")
|
||||
.ok_or(Error::NoHeaderOrCookieFound)?;
|
||||
|
||||
Ok(cookie.value().to_string())
|
||||
}
|
||||
|
||||
pub fn get_claims_from_req(req: &HttpRequest) -> Result<Claims, Error> {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user