mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
Compare commits
4 Commits
a0a1098b8c
...
e02cc4bca7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e02cc4bca7 | ||
|
d1ef5c032e
|
|||
|
b635750e28
|
|||
|
a59fff927d
|
636
Cargo.lock
generated
636
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -18,63 +18,63 @@ providers = { path = "providers" }
|
||||
actix-macros = { path = "actix-macros" }
|
||||
|
||||
# serve api
|
||||
actix-web = "4.11.0"
|
||||
actix-web = "4"
|
||||
|
||||
# basic
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
derive_more = { version = "2.0.1", features = ["full"] }
|
||||
dotenvy = "0.15.7"
|
||||
chrono = { version = "0", features = ["serde"] }
|
||||
derive_more = { version = "2", features = ["full"] }
|
||||
dotenvy = "0"
|
||||
|
||||
# sql
|
||||
database = { path = "database" }
|
||||
|
||||
# logging
|
||||
env_logger = "0.11.8"
|
||||
env_logger = "0"
|
||||
|
||||
# async
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
tokio-util = "0.7.16"
|
||||
futures-util = "0.3.31"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
tokio-util = "0"
|
||||
futures-util = "0"
|
||||
|
||||
# authorization
|
||||
bcrypt = "0.17.1"
|
||||
jsonwebtoken = { version = "10.1.0", features = ["use_pem"] }
|
||||
bcrypt = "0"
|
||||
jsonwebtoken = { version = "10", features = ["use_pem"] }
|
||||
|
||||
# creating users
|
||||
objectid = "0.2.0"
|
||||
objectid = "0"
|
||||
|
||||
# schedule downloader
|
||||
reqwest = { version = "0.12.23", features = ["json"] }
|
||||
mime = "0.3.17"
|
||||
reqwest = { version = "0", features = ["json"] }
|
||||
mime = "0"
|
||||
|
||||
# error handling
|
||||
sentry = "0.43.0"
|
||||
sentry-actix = "0.43.0"
|
||||
sentry = "0"
|
||||
sentry-actix = "0"
|
||||
|
||||
# [de]serializing
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_with = "3.14"
|
||||
serde_with = "3"
|
||||
|
||||
sha1 = "0.11.0-rc.2"
|
||||
|
||||
# documentation
|
||||
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono"] }
|
||||
utoipa-rapidoc = { version = "6.0.0", features = ["actix-web"] }
|
||||
utoipa-actix-web = "0.1.2"
|
||||
utoipa = { version = "5", features = ["actix_extras", "chrono"] }
|
||||
utoipa-rapidoc = { version = "6", features = ["actix-web"] }
|
||||
utoipa-actix-web = "0"
|
||||
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
hex-literal = "1"
|
||||
log = "0.4.28"
|
||||
log = "0"
|
||||
|
||||
# telegram webdata deciding and verify
|
||||
base64 = "0.22.1"
|
||||
percent-encoding = "2.3.2"
|
||||
base64 = "0"
|
||||
percent-encoding = "2"
|
||||
ed25519-dalek = "3.0.0-pre.1"
|
||||
|
||||
# development tracing
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
console-subscriber = { version = "0", optional = true }
|
||||
tracing = { version = "0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
providers = { path = "providers", features = ["test"] }
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
migration = { path = "migration" }
|
||||
entity = { path = "entity" }
|
||||
sea-orm = { version = "2.0.0-rc.6", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||
sea-orm = { version = "2.0.0-rc.15", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||
|
||||
paste = "1.0.15"
|
||||
paste = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -3,4 +3,5 @@
|
||||
pub mod prelude;
|
||||
|
||||
pub mod sea_orm_active_enums;
|
||||
pub mod service_user;
|
||||
pub mod user;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.12
|
||||
|
||||
pub use super::service_user::Entity as ServiceUser;
|
||||
pub use super::user::Entity as User;
|
||||
|
||||
16
database/entity/src/service_user.rs
Normal file
16
database/entity/src/service_user.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.12
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "service_user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -12,7 +12,7 @@ path = "src/lib.rs"
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "2.0.0-rc.6"
|
||||
version = "2.0.0-rc.15"
|
||||
features = [
|
||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||
|
||||
@@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::MigratorTrait;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20250904_024854_init;
|
||||
mod m20251027_230335_add_service_users;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20250904_024854_init::Migration),
|
||||
Box::new(m20251027_230335_add_service_users::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
33
database/migration/src/m20251027_230335_add_service_users.rs
Normal file
33
database/migration/src/m20251027_230335_add_service_users.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ServiceUser::Table)
|
||||
.if_not_exists()
|
||||
.col(string_uniq(ServiceUser::Id).primary_key().not_null())
|
||||
.col(string(ServiceUser::Name))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(ServiceUser::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ServiceUser {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
}
|
||||
@@ -4,7 +4,28 @@ pub use migration;
|
||||
pub use sea_orm;
|
||||
|
||||
pub mod entity {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use entity::*;
|
||||
|
||||
pub use entity::user::{ActiveModel as ActiveUser, Model as User, Entity as UserEntity, Column as UserColumn};
|
||||
pub use entity::user::{
|
||||
ActiveModel as ActiveUser, //
|
||||
Column as UserColumn, //
|
||||
Entity as UserEntity, //
|
||||
Model as User, //
|
||||
};
|
||||
|
||||
pub use entity::service_user::{
|
||||
ActiveModel as ActiveServiceUser, //
|
||||
Column as ServiceUserColumn, //
|
||||
Entity as ServiceUserEntity, //
|
||||
Model as ServiceUser, //
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum UserType {
|
||||
Default,
|
||||
Service,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ macro_rules! define_find_by {
|
||||
}
|
||||
|
||||
impl Query {
|
||||
// User
|
||||
|
||||
define_find_by!(user, id, str, Id);
|
||||
define_find_by!(user, telegram_id, i64, TelegramId);
|
||||
define_find_by!(user, vk_id, i32, VkId);
|
||||
@@ -60,4 +62,12 @@ impl Query {
|
||||
define_is_exists!(user, username, str, Username);
|
||||
define_is_exists!(user, telegram_id, i64, TelegramId);
|
||||
define_is_exists!(user, vk_id, i32, VkId);
|
||||
|
||||
// Service user
|
||||
|
||||
define_find_by!(service_user, id, str, Id);
|
||||
define_find_by!(service_user, name, str, Name);
|
||||
|
||||
define_is_exists!(service_user, id, str, Id);
|
||||
define_is_exists!(service_user, name, str, Name);
|
||||
}
|
||||
|
||||
@@ -9,23 +9,23 @@ test = []
|
||||
[dependencies]
|
||||
base = { path = "../base" }
|
||||
|
||||
tokio = { version = "1.47.1", features = ["sync", "macros", "time"] }
|
||||
tokio-util = "0.7.16"
|
||||
tokio = { version = "1", features = ["sync", "macros", "time"] }
|
||||
tokio-util = "0"
|
||||
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
chrono = { version = "0", features = ["serde"] }
|
||||
|
||||
derive_more = { version = "2.0.1", features = ["error", "display", "from"] }
|
||||
derive_more = { version = "2", features = ["error", "display", "from"] }
|
||||
|
||||
utoipa = { version = "5.4.0", features = ["macros", "chrono"] }
|
||||
utoipa = { version = "5", features = ["macros", "chrono"] }
|
||||
|
||||
calamine = "0.31"
|
||||
async-trait = "0.1.89"
|
||||
calamine = "0"
|
||||
async-trait = "0"
|
||||
|
||||
reqwest = "0.12.23"
|
||||
ua_generator = "0.5.22"
|
||||
regex = "1.11.2"
|
||||
strsim = "0.11.1"
|
||||
log = "0.4.27"
|
||||
sentry = "0.43.0"
|
||||
fancy-regex = "0.16.2"
|
||||
reqwest = "0"
|
||||
ua_generator = "0"
|
||||
regex = "1"
|
||||
strsim = "0"
|
||||
log = "0"
|
||||
sentry = "0"
|
||||
fancy-regex = "0"
|
||||
|
||||
|
||||
@@ -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