16 Commits

Author SHA1 Message Date
dependabot[bot]
7b2f9cb684 chore(deps): bump console-subscriber from 0.4.1 to 0.5.0
Bumps [console-subscriber](https://github.com/tokio-rs/console) from 0.4.1 to 0.5.0.
- [Release notes](https://github.com/tokio-rs/console/releases)
- [Changelog](https://github.com/tokio-rs/console/blob/main/release-plz.toml)
- [Commits](https://github.com/tokio-rs/console/compare/console-subscriber-v0.4.1...console-subscriber-v0.5.0)

---
updated-dependencies:
- dependency-name: console-subscriber
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 19:07:27 +00:00
2442641479 feat(ci): auto-deploy new version after building docker image 2025-10-29 02:31:13 +04:00
ac16c96e5e chore(schedule): add lesson type 'differentiated exam' 2025-10-29 02:16:25 +04:00
622464e4c3 feat(users): add endpoints for getting user by ids 2025-10-28 22:33:49 +04:00
39c60ef939 feat(middleware): add support of path patterns 2025-10-28 22:33:10 +04:00
d1ef5c032e feat: implement service users 2025-10-28 06:53:31 +04:00
b635750e28 feat(db): add service users table 2025-10-28 06:46:30 +04:00
a59fff927d chore(deps): update dependencies 2025-10-28 06:45:55 +04:00
cdc89b5bcd fix(parser): fix sentry error sending 2025-10-10 03:00:47 +04:00
ad86f6cd64 feat(parser): limit names regex to maximum 2 elements
This allows us to not worry about subgroups array index overflows, and we can make better non-standard case solving.
2025-10-10 01:39:54 +04:00
a3b4a501db feat(parser): improve names regex to exclude some non-standard cases
Like "Название ФАмилия. И.О.".
In that case regex will grab "Название ФА", instead of "Амилия. И. О." (we can't add 'Ф', bc it will make regex checks way more complex).

Now it will ignore "Название ФА" if after that lower or upper char is placed.
Previously only lower chars are excluded and check won't exclude "Название ФА" and grabs "Название Ф" bc after 'Ф' uppercase char is present.
2025-10-10 01:37:52 +04:00
df0e99a4d0 feat(parser): make lesson cell range less strict to support upcoming split-lessons 2025-10-10 01:31:55 +04:00
a8cf8fb0f5 feat(parser): improve street regex 2025-10-10 01:30:56 +04:00
7ed866138e feat(error): add error for unknown lesson type 2025-10-10 01:30:30 +04:00
7bac48f8fc feat(error): add more intuitive CellPos formatting and get rid of ErrorCell 2025-10-10 01:27:05 +04:00
191ec36fef chore: remove useless commented code 2025-10-10 01:25:12 +04:00
36 changed files with 1070 additions and 662 deletions

View File

@@ -140,3 +140,6 @@ jobs:
cache-to: type=gha,mode=max
build-args: |
"BINARY_NAME=${{ env.BINARY_NAME }}"
- name: Deploy
run: curl ${{ secrets.DEPLOY_URL }}

719
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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 = "9.3.1", features = ["use_pem"] }
bcrypt = "0"
jsonwebtoken = { version = "9", 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"] }

View File

@@ -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"] }

View File

@@ -3,4 +3,5 @@
pub mod prelude;
pub mod sea_orm_active_enums;
pub mod service_user;
pub mod user;

View File

@@ -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;

View 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 {}

View File

@@ -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.

View File

@@ -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),
]
}
}

View 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,
}

View File

@@ -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,
}
}

View File

@@ -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);
}

View File

@@ -102,7 +102,10 @@ pub enum LessonType {
CourseProjectDefense,
/// Практическое занятие.
Practice
Practice,
/// Дифференцированный зачёт.
DifferentiatedExam,
}
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
@@ -212,70 +215,6 @@ impl ScheduleSnapshot {
}
}
// #[derive(Clone, Debug, Display, Error, ToSchema)]
// #[display("row {row}, column {column}")]
// pub struct ErrorCellPos {
// pub row: u32,
// pub column: u32,
// }
//
// #[derive(Clone, Debug, Display, Error, ToSchema)]
// #[display("'{data}' at {pos}")]
// pub struct ErrorCell {
// pub pos: ErrorCellPos,
// pub data: String,
// }
//
// impl ErrorCell {
// pub fn new(row: u32, column: u32, data: String) -> Self {
// Self {
// pos: ErrorCellPos { row, column },
// data,
// }
// }
// }
// #[derive(Clone, Debug, Display, Error, ToSchema)]
// pub enum ParseError {
// /// Errors related to reading XLS file.
// #[display("{_0:?}: Failed to read XLS file.")]
// #[schema(value_type = String)]
// BadXLS(Arc<calamine::XlsError>),
//
// /// Not a single sheet was found.
// #[display("No work sheets found.")]
// NoWorkSheets,
//
// /// There are no data on the boundaries of the sheet.
// #[display("There is no data on work sheet boundaries.")]
// UnknownWorkSheetRange,
//
// /// Failed to read the beginning and end of the lesson from the cell
// #[display("Failed to read lesson start and end from {_0}.")]
// LessonBoundaries(ErrorCell),
//
// /// Not found the beginning and the end corresponding to the lesson.
// #[display("No start and end times matching the lesson (at {_0}) was found.")]
// LessonTimeNotFound(ErrorCellPos),
// }
//
// impl Serialize for ParseError {
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: Serializer,
// {
// match self {
// ParseError::BadXLS(_) => serializer.serialize_str("BAD_XLS"),
// ParseError::NoWorkSheets => serializer.serialize_str("NO_WORK_SHEETS"),
// ParseError::UnknownWorkSheetRange => {
// serializer.serialize_str("UNKNOWN_WORK_SHEET_RANGE")
// }
// ParseError::LessonBoundaries(_) => serializer.serialize_str("GLOBAL_TIME"),
// ParseError::LessonTimeNotFound(_) => serializer.serialize_str("LESSON_TIME_NOT_FOUND"),
// }
// }
// }
#[async_trait]
pub trait ScheduleProvider
where

View File

@@ -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"

View File

@@ -1,21 +1,5 @@
use derive_more::{Display, Error, From};
use crate::parser::worksheet::CellPos;
#[derive(Clone, Debug, Display, Error)]
#[display("'{data}' at {pos}")]
pub struct ErrorCell {
pub pos: CellPos,
pub data: String,
}
impl ErrorCell {
pub fn new(row: u32, column: u32, data: &str) -> Self {
Self {
pos: CellPos { row, column },
data: data.to_string(),
}
}
}
use derive_more::{Display, Error, From};
#[derive(Debug, Display, Error, From)]
pub enum Error {
@@ -28,11 +12,14 @@ pub enum Error {
#[display("There is no data on work sheet boundaries.")]
UnknownWorkSheetRange,
#[display("Failed to read lesson start and end from {_0}.")]
NoLessonBoundaries(ErrorCell),
#[display("Failed to read lesson start and end of lesson at {_0}.")]
NoLessonBoundaries(CellPos),
#[display("No start and end times matching the lesson (at {_0}) was found.")]
LessonTimeNotFound(CellPos),
#[display("Unknown lesson type `{type}` at {pos}")]
UnknownLessonType { pos: CellPos, r#type: String },
}
pub type Result<T> = core::result::Result<T, Error>;

View File

@@ -1,6 +1,5 @@
pub use self::error::{Error, Result};
use crate::or_continue;
use crate::parser::error::ErrorCell;
use crate::parser::worksheet::{CellPos, CellRange, WorkSheet};
use crate::parser::LessonParseResult::{Lessons, Street};
use base::LessonType::Break;
@@ -188,6 +187,7 @@ fn guess_lesson_type(text: &str) -> Option<LessonType> {
("курсовой проект", LessonType::CourseProject),
("защита курсового проекта", LessonType::CourseProjectDefense),
("практическое занятие", LessonType::Practice),
("дифференцированный зачет", LessonType::DifferentiatedExam),
])
});
@@ -217,7 +217,7 @@ fn parse_lesson(
};
static OTHER_STREET_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[А-Я][а-я]+[,\s]\d+$").unwrap());
LazyLock::new(|| Regex::new(r"^[А-Я][а-я]+[,\s]+д\.\s\d+$").unwrap());
if OTHER_STREET_RE.is_match(&cell_data) {
return Ok(Street(cell_data));
@@ -226,12 +226,17 @@ fn parse_lesson(
cell_data
};
let cell_range = worksheet.get_merge_from_start(row, group_column);
let lesson_cell_range = worksheet.get_merge_from_start(row, group_column);
let (default_range, lesson_time) = {
let end_time_arr = day_boundaries
.iter()
.filter(|time| time.range.end.row == cell_range.end.row)
.filter(
|BoundariesData {
range: CellRange { end, .. },
..
}| { lesson_cell_range.end.row <= end.row },
)
.collect::<Vec<&BoundariesData>>();
let end_time = end_time_arr
@@ -257,12 +262,12 @@ fn parse_lesson(
name,
mut subgroups,
r#type: lesson_type,
} = parse_name_and_subgroups(&name)?;
} = parse_name_and_subgroups(&name, row, group_column)?;
{
let cabinets: Vec<String> = parse_cabinets(
worksheet,
(cell_range.start.row, cell_range.end.row),
(lesson_cell_range.start.row, lesson_cell_range.end.row),
group_column + 1,
);
@@ -364,7 +369,7 @@ struct ParsedLessonName {
//noinspection GrazieInspection
/// Getting the "pure" name of the lesson and list of teachers from the text of the lesson cell.
fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
fn parse_name_and_subgroups(text: &str, row: u32, column: u32) -> Result<ParsedLessonName> {
// Части названия пары:
// 1. Само название.
// 2. Список преподавателей и подгрупп.
@@ -373,7 +378,7 @@ fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
// Регулярное выражение для получения ФИО преподавателей и номеров подгрупп (aka. второй части).
static NAME_RE: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
fancy_regex::Regex::new(
r"([А-Я][а-я]+(?:[\s.]*[А-Я]){1,2})(?=[^а-я])[.\s]*(?:\(?(\d)[\sа-я]*\)?)?",
r"([А-Я][а-я]+(?:[\s.]*[А-Я]){1,2})(?=[^Аа-я])[.\s]*(?:\(?(\d)[\sа-я]*\)?)?",
)
.unwrap()
});
@@ -394,10 +399,10 @@ fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
let mut lesson_name: Option<&str> = None;
let mut extra: Option<&str> = None;
let mut shared_subgroup = false;
let mut shared_subgroup = true;
let mut subgroups: [Option<LessonSubGroup>; 2] = [None, None];
for capture in NAME_RE.captures_iter(&text) {
for capture in NAME_RE.captures_iter(&text).take(2) {
let capture = capture.unwrap();
if lesson_name.is_none() {
@@ -438,17 +443,23 @@ fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
match subgroup_index {
None => {
subgroups[0] = subgroup;
subgroups[1] = None;
shared_subgroup = true;
break;
// we have only 2 matches max so more than 2 subgroups we cant have 100%
*subgroups.iter_mut().find(|x| x.is_none()).unwrap() = subgroup;
}
Some(num) => {
// bc we have indexed subgroup
shared_subgroup = false;
// 1 - 1 = 0 | 2 - 1 = 1 | 3 - 1 = 2 (schedule index to array index)
// 0 % 2 = 0 | 1 % 2 = 1 | 2 % 2 = 0 (clamp)
let normalised = (num - 1) % 2;
let subgroup_index = ((num - 1) % 2) as usize;
subgroups[normalised as usize] = subgroup;
// if we have subgroup in that index (probably non-indexed, we change it index to free)
if subgroups[subgroup_index].is_some() {
subgroups.swap(0, 1);
}
subgroups[subgroup_index] = subgroup;
}
}
}
@@ -456,7 +467,7 @@ fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
let subgroups = if lesson_name.is_none() {
Vec::new()
} else if shared_subgroup {
Vec::from([subgroups[0].take()])
Vec::from([subgroups.into_iter().next().unwrap()])
} else {
Vec::from(subgroups)
};
@@ -475,13 +486,19 @@ fn parse_name_and_subgroups(text: &str) -> Result<ParsedLessonName> {
if result.is_none() {
#[cfg(not(debug_assertions))]
sentry::capture_message(
&format!("Не удалось угадать тип пары '{}'!", extra),
sentry::Level::Warning,
);
sentry::capture_error(&Error::UnknownLessonType {
r#type: extra.to_string(),
pos: CellPos::new(row, column),
});
#[cfg(debug_assertions)]
log::warn!("Не удалось угадать тип пары '{}'!", extra);
log::warn!(
"{}",
Error::UnknownLessonType {
r#type: extra.to_string(),
pos: CellPos::new(row, column),
}
);
}
result
@@ -548,9 +565,8 @@ fn parse_day_boundaries(
continue;
};
let lesson_time = parse_lesson_boundaries_cell(&time_cell, date).ok_or(
Error::NoLessonBoundaries(ErrorCell::new(row, column, &time_cell)),
)?;
let lesson_time = parse_lesson_boundaries_cell(&time_cell, date)
.ok_or(Error::NoLessonBoundaries(CellPos::new(row, column)))?;
// type
let lesson_type = if time_cell.contains("пара") {

View File

@@ -1,5 +1,5 @@
use derive_more::Display;
use regex::Regex;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::sync::LazyLock;
@@ -9,13 +9,35 @@ pub struct WorkSheet {
pub merges: Vec<calamine::Dimensions>,
}
#[derive(Clone, Debug, Display, derive_more::Error)]
#[display("row {row}, column {column}")]
#[derive(Clone, Debug, derive_more::Error)]
pub struct CellPos {
pub row: u32,
pub column: u32,
}
fn format_column_index(index: u32) -> String {
// https://stackoverflow.com/a/297214
let quotient = index / 26;
let char = char::from((65 + (index % 26)) as u8);
if quotient > 0 {
return format!("{}{}", format_column_index(quotient - 1), char);
}
char.to_string()
}
impl Display for CellPos {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"column {}, row {}",
format_column_index(self.column),
self.row + 1,
))
}
}
pub struct CellRange {
pub start: CellPos,
pub end: CellPos,

View File

@@ -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),
}
})
}
}

View File

@@ -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,64 @@ 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()
.add_paths(
["/by/id/{id}", "/by/telegram-id/{id}"],
Some(ServiceConfig {
allow_service: true,
user_roles: Some(&[UserRole::Admin]),
}),
)
.build(),
)
.service(
utoipa_actix_web::scope("/by")
.service(routes::users::by::by_id)
.service(routes::users::by::by_telegram_id),
)
.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 +115,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)

View File

@@ -1,18 +1,68 @@
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::entity::UserType;
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;
#[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 +80,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 +100,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);
}
fn should_skip(&self, req: &ServiceRequest) -> bool {
let path = req.match_info().unprocessed();
self.ignore.iter().any(|ignore| {
if !path.starts_with(ignore) {
return false;
return Ok(());
}
if let Some(other) = path.as_bytes().get(ignore.len()) {
return [b'?', b'/'].contains(other);
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);
}
true
})
match Query::is_service_user_exists_by_id(db, &claims.id).await {
Ok(true) => Ok(()),
_ => Err(authorized_user::Error::NoUser),
}
}
}
}
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(&current_path) {
continue;
}
return config.clone();
}
}
default.clone()
}
}
@@ -87,15 +178,33 @@ 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 match_info = req.match_info();
let path = if let Some(pattern) = req.match_pattern() {
let scope_start_idx = match_info
.as_str()
.find(match_info.unprocessed())
.unwrap_or(0);
pattern.as_str().split_at(scope_start_idx).1.to_owned()
} else {
match_info.unprocessed().to_owned()
};
let Some(config) = Self::find_config(&path, &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
View File

@@ -0,0 +1 @@
pub mod service_users;

View 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,
}
}

View File

@@ -0,0 +1,3 @@
mod create;
pub use create::*;

View File

@@ -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())),

View File

@@ -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))
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -1,3 +1,4 @@
pub mod admin;
pub mod auth;
pub mod flow;
pub mod schedule;

View 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,
}
}

View File

@@ -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::*;

View File

@@ -163,6 +163,7 @@ pub mod user {
#[schema(examples(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY"
))]
#[serde(skip_serializing_if = "Option::is_none")]
pub access_token: Option<String>,
}

56
src/routes/users/by.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::routes::schema::user::UserResponse;
use crate::routes::users::by::schema::{ErrorCode, ServiceResponse};
use crate::state::AppState;
use actix_web::{get, web};
use database::query::Query;
#[utoipa::path(responses((status = OK, body = UserResponse)))]
#[get("/id/{id}")]
pub async fn by_id(app_state: web::Data<AppState>, path: web::Path<String>) -> ServiceResponse {
let user_id = path.into_inner();
let db = app_state.get_database();
match Query::find_user_by_id(db, &user_id).await {
Ok(Some(user)) => Ok(UserResponse::from(user)),
_ => Err(ErrorCode::NotFound),
}
.into()
}
#[utoipa::path(responses((status = OK, body = UserResponse)))]
#[get("/telegram-id/{id}")]
pub async fn by_telegram_id(
app_state: web::Data<AppState>,
path: web::Path<i64>,
) -> ServiceResponse {
let telegram_id = path.into_inner();
let db = app_state.get_database();
match Query::find_user_by_telegram_id(db, telegram_id).await {
Ok(Some(user)) => Ok(UserResponse::from(user)),
_ => Err(ErrorCode::NotFound),
}
.into()
}
mod schema {
use crate::routes::schema::user::UserResponse;
use actix_macros::ErrResponse;
use derive_more::Display;
use serde::Serialize;
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = Users::By::ErrorCode)]
pub enum ErrorCode {
/// User not found.
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
#[display("Required user not found.")]
NotFound,
}
}

View File

@@ -1,3 +1,4 @@
pub mod by;
mod change_group;
mod change_username;
mod me;

View File

@@ -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

View File

@@ -1,2 +1,3 @@
pub mod jwt;
pub mod telegram;
pub mod req_auth;

56
src/utility/req_auth.rs Normal file
View 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)
}